diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..9e42524d5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.8.0-beta] - 2020-03-31 +### Added +- Initial public beta release +- aws-apigateway-dynamodb module added +- aws-apigateway-lambda module added +- aws-apigateway-sqs module added +- aws-cloudfront-apigateway-lambda module added +- aws-cloudfront-apigateway module added +- aws-cloudfront-s3 module added +- aws-cognito-apigateway-lambda module added +- aws-dynamodb-stream-lambda-elasticsearch-kibana module added +- aws-dynamodb-stream-lambda module added +- aws-events-rule-lambda module added +- aws-iot-kinesisfirehose-s3 module added +- aws-iot-lambda-dynamodb module added +- aws-iot-lambda module added +- aws-kinesisfirehose-s3-and-kinesisanalytics module added +- aws-kinesisfirehose-s3 module added +- aws-kinesisstreams-lambda module added +- aws-lambda-dynamodb module added +- aws-lambda-elasticsearch-kibana module added +- aws-lambda-s3 module added +- aws-lambda-sns module added +- aws-s3-lambda module added +- aws-sns-lambda module added +- aws-sqs-lambda module added +- core module added diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5b627cfa6..3b6446687 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,4 +1,4 @@ ## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 914e0741d..79071822b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ information to effectively respond to your bug report or contribution. We welcome you to use the GitHub issue tracker to report bugs or suggest features. -When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already +When filing an issue, please check [existing open](https://github.com/awslabs/aws-solutions-konstruk/issues), or [recently closed](https://github.com/awslabs/aws-solutions-konstruk/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: * A reproducible test case or series of steps @@ -41,7 +41,7 @@ GitHub provides additional document on [forking a repository](https://help.githu ## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/aws-solutions-konstruk/labels/help%20wanted) issues is a great place to start. ## Code of Conduct @@ -56,6 +56,6 @@ If you discover a potential security issue in this project we ask that you notif ## Licensing -See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. +See the [LICENSE](https://github.com/awslabs/aws-solutions-konstruk/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/NOTICE b/NOTICE index 616fc5889..135629617 100644 --- a/NOTICE +++ b/NOTICE @@ -1 +1,47 @@ -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +AWS Konstruk +Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +Licensed under the Apache License Version 2.0 (the "License"). You may not use this file except +in compliance with the License. A copy of the License is located at http://www.apache.org/licenses/ +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the +specific language governing permissions and limitations under the License. + +********************** +THIRD PARTY COMPONENTS +********************** +This software includes third party software subject to the following copyrights: + +@types/jest under the Massachusetts Institute of Technology (MIT) license +@types/node under the Massachusetts Institute of Technology (MIT) license +@typescript-eslint/eslint-plugin under the Massachusetts Institute of Technology (MIT) license +@typescript-eslint/parser under the BSD-2-Clause license +aws-cdk under the Apache License Version 2.0 +aws-sdk under the Apache License Version 2.0 +aws-sdk-mock under the Apache License Version 2.0 +bootstrap under the Massachusetts Institute of Technology (MIT) license +chai under the Massachusetts Institute of Technology (MIT) license +color under the Massachusetts Institute of Technology (MIT) license +color-name under the Massachusetts Institute of Technology (MIT) license +deepmerge under the MIT License +eslint under the Massachusetts Institute of Technology (MIT) license +eslint-import-resolver-node under the Massachusetts Institute of Technology (MIT) license +eslint-import-resolver-typescript under the ISC license +eslint-plugin-import under the Massachusetts Institute of Technology (MIT) license +eslint-plugin-license-header under the Massachusetts Institute of Technology (MIT) license +fs-extra under the Massachusetts Institute of Technology (MIT) license +jest under the Massachusetts Institute of Technology (MIT) license +jsii under the Apache License Version 2.0 +jsii-pacmak under the Apache License Version 2.0 +lerna under the Massachusetts Institute of Technology (MIT) license +minimist under the Massachusetts Institute of Technology (MIT) license +mocha under the Massachusetts Institute of Technology (MIT) license +moment under the Massachusetts Institute of Technology (MIT) license +npm-run-all under the Massachusetts Institute of Technology (MIT) license +nyc under the ISC license +sharp under the Apache License Version 2.0 +sinon under the BSD-3-Clause license +sinon-chai under the BSD-2-Clause license +source-map-support under the Massachusetts Institute of Technology (MIT) license +tslint under the Apache License Version 2.0 +typescript under the Apache License Version 2.0 +uuid under the Massachusetts Institute of Technology (MIT) license \ No newline at end of file diff --git a/README.md b/README.md index 8f46b7c6c..e0bce0513 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,60 @@ -## My Project +# API Reference + -TODO: Fill this README out! +--- -Be sure to: +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) -* Change the title in this README -* Edit your repository description on GitHub +> **This is a _developer preview_ (public beta) library.** +> +> All modules are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. -## License +--- + -This project is licensed under the Apache-2.0 License. +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/| +|:-------------|:-------------| +
+The AWS Solutions Konstruk Library (Konstruk) is an open-source extension of the AWS Cloud Development Kit (AWS CDK) that provides multi-service, well-architected patterns for quickly defining solutions in code to create predictable and repeatable infrastructure. Konstruk's goal is to accelerates the experience for developers to build solutions of any size using pattern-based definitions for their architecture. + +The patterns defined in Konstruk are high level, multi-service abstractions of AWS CDK constructs that have default configurations based on well-architected best practices. The library is organized into logical modules using object-oriented techniques to create each architectural pattern model. + +The CDK is available in the following languages: + +* JavaScript, TypeScript (Node.js ≥ 10.3.0) +* Python (Python ≥ 3.6) + +## Modules + +The Konstruk library is organized into several modules. They are named like this: + +* __aws-xxx__: well architected pattern package for the indicated services. This package will contain constructs that contain multiple AWS CDK service modules to configure the given pattern. +* __xxx__: packages that don't start "aws-" are Konstruk core modules that are used to configure best practice defaults for services used within the pattern library. + +## Module Contents + +Modules contain the following types: + +* __Patterns__ - All higher-level, multi-services constructs in this library. +* __Other Types__ - All non-construct classes, interfaces, structs and enums that exist to support the patterns. + +Patterns take a set of (input) properties in their constructor; the set of properties (and which ones are required) can be seen on a pattern's documentation page. + +The pattern's documentation page also lists the available methods to call and the properties which can be used to retrieve information about the pattern after it has been instantiated. + +## Sample Use Cases + +This library includes a collection of functional use case implementations to demonstrate the usage of Konstruk architectural patterns. These can be used in the same way as architectural patterns, and can be conceptualized as an additional "higher-level" abstraction of those patterns. The following use cases are provided as functional examples: + +* __aws-s3-static-website__ - implements an Amazon CloudFront distribution, Amazon S3 bucket and AWS Lambda-based custom resource to copy the static website content for the Wild Rydes demo website (part of the aws-serverless-web-app implementation). + * Use case pattern: https://github.com/awslabs/aws-solutions-konstruk/source/use_cases/aws-s3-static-website +* __aws-serverless-image-handler__ - implements an Amazon CloudFront distribution, an Amazon API Gateway REST API, an AWS Lambda function, and necessary permissions/logic to provision a functional image handler API for serving image content from one or more Amazon S3 buckets within the deployment account. + * Use case pattern: https://github.com/awslabs/aws-solutions-konstruk/source/use_cases/aws-serverless-image-handler +* __aws-serverless-web-app__ - implements a simple serverless web application that enables users to request unicorn rides from the Wild Rydes fleet. The application will present users with an HTML based user interface for indicating the location where they would like to be picked up and will interface on the backend with a RESTful web service to submit the request and dispatch a nearby unicorn. The application will also provide facilities for users to register with the service and log in before requesting rides. + * Use case pattern: https://github.com/awslabs/aws-solutions-konstruk/source/use_cases/aws-serverless-web-app + +*** +© Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/lerna.json b/source/lerna.json new file mode 100644 index 000000000..80d43a175 --- /dev/null +++ b/source/lerna.json @@ -0,0 +1,11 @@ +{ + "lerna": "3.15.0", + "npmClient": "yarn", + "useWorkspaces": true, + "packages": [ + "./patterns/@aws-solutions-konstruk/*", + "./use_cases/*" + ], + "rejectCycles": "true", + "version": "0.8.0" +} diff --git a/source/package.json b/source/package.json new file mode 100644 index 000000000..8523bf35e --- /dev/null +++ b/source/package.json @@ -0,0 +1,44 @@ +{ + "name": "aws-solutions-konstruk", + "version": "0.8.0", + "description": "AWS Solutions Konstruk Library", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source" + }, + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "^2.14.0", + "@typescript-eslint/parser": "^2.14.0", + "eslint": "^6.8.0", + "eslint-import-resolver-node": "^0.3.2", + "eslint-import-resolver-typescript": "^2.0.0", + "eslint-plugin-import": "^2.19.1", + "eslint-plugin-license-header": "^0.2.0", + "fs-extra": "^8.1.0", + "jest": "^24.9.0", + "jsii": "~0.22.0", + "jsii-pacmak": "~0.22.0", + "tslint": "^5.20.1", + "typescript": "~3.8.2" + }, + "devDependencies": { + "lerna": "^3.18.4" + }, + "workspaces": { + "packages": [ + "./patterns/@aws-solutions-konstruk/*", + "./use_cases/*" + ], + "nohoist": [ + "**/deepmerge", + "**/deepmerge/**" + ] + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/README.md b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/README.md new file mode 100644 index 000000000..ce68175ce --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/README.md @@ -0,0 +1,77 @@ +# aws-apigateway-dynamodb module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-apigateway-dynamodb/| +|:-------------|:-------------| +
+ + +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_apigateway_dynamodb`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-apigateway-dynamodb`| + +## Overview +This AWS Solutions Konstruk implements an Amazon API Gateway REST API connected to Amazon DynamoDB table. + +Here is a minimal deployable pattern definition: + +``` javascript +import { ApiGatewayToDynamoDBProps, ApiGatewayToDynamoDB } from "@aws-solutions-konstruk/aws-apigateway-dynamodb"; + +const props: ApiGatewayToDynamoDBProps = {}; + +new ApiGatewayToDynamoDB(stack, 'test-api-gateway-dynamodb-default', props); + +``` + +## Initializer + +``` text +new ApiGatewayToDynamoDB(scope: Construct, id: string, props: ApiGatewayToDynamoDBProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`ApiGatewayToDynamoDBProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|dynamoTableProps|[`dynamodb.TableProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.TableProps.html)|Optional user provided props to override the default props for DynamoDB Table| +|apiGatewayProps?|[`api.RestApiProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.RestApiProps.html)|Optional user-provided props to override the default props for the API Gateway.| +|allowCreateOperation|`boolean`|Whether to deploy API Gateway Method for Create operation on Dynamodb DB table.| +|createRequestTemplate|`string`|API Gateway Request template for Create method, required if allowCreateOperation set to true| +|allowReadOperation|`boolean`|Whether to deploy API Gateway Method for Read operation on Dynamodb DB table.| +|allowUpdateOperation|`boolean`|Whether to deploy API Gateway Method for Update operation on Dynamodb DB table.| +|updateRequestTemplate|`string`|API Gateway Request template for Update method, required if allowUpdateOperation set to true| +|allowDeleteOperation|`boolean`|Whether to deploy API Gateway Method for Delete operation on Dynamodb DB table.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|restApi()|[`api.RestApi`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.RestApi.html)|Returns an instance of the api.RestApi created by the construct.| +|dynamoTable()|[`dynamodb.Table`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.Table.html)|Retruns an instance of dynamodb.Table created by the construct.| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/architecture.png new file mode 100644 index 000000000..4397dc3dc Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/lib/index.ts new file mode 100644 index 000000000..78fa5984c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/lib/index.ts @@ -0,0 +1,226 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as api from '@aws-cdk/aws-apigateway'; +import * as iam from '@aws-cdk/aws-iam'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import { overrideProps } from '@aws-solutions-konstruk/core'; + +/** + * @summary The properties for the ApiGatewayToSqs class. + */ +export interface ApiGatewayToDynamoDBProps { + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly dynamoTableProps?: dynamodb.TableProps, + /** + * Optional user-provided props to override the default props for the API Gateway. + * + * @default - Default properties are used. + */ + readonly apiGatewayProps?: api.RestApiProps | any, + + /** + * Whether to deploy API Gateway Method for Create operation on Dynamodb DB table. + * + * @default - false + */ + readonly allowCreateOperation?: boolean, + + /** + * API Gateway Request template for Create method, required if allowCreateOperation set to true + * + * @default - None + */ + readonly createRequestTemplate?: string, + + /** + * Whether to deploy API Gateway Method for Read operation on Dynamodb DB table. + * + * @default - true + */ + readonly allowReadOperation?: boolean, + + /** + * Whether to deploy API Gateway Method for Update operation on Dynamodb DB table. + * + * @default - false + */ + readonly allowUpdateOperation?: boolean, + + /** + * API Gateway Request template for Update method, required if allowUpdateOperation set to true + * + * @default - None + */ + readonly updateRequestTemplate?: string, + + /** + * Whether to deploy API Gateway Method for Delete operation on Dynamodb DB table. + * + * @default - false + */ + readonly allowDeleteOperation?: boolean +} + +/** + * @summary The ApiGatewayToDynamoDB class. + */ +export class ApiGatewayToDynamoDB extends Construct { + private table: dynamodb.Table; + private apiGatewayRole: iam.Role; + private apiGateway: api.RestApi; + + /** + * @summary Constructs a new instance of the ApiGatewayToSqs class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {CloudFrontToApiGatewayToLambdaProps} props - user provided props for the construct. + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: ApiGatewayToDynamoDBProps) { + super(scope, id); + let partitionKeyName: string; + + // Set the default props for DynamoDB table + if (props.dynamoTableProps) { + const dynamoTableProps: dynamodb.TableProps = overrideProps(defaults.DefaultTableProps, props.dynamoTableProps); + partitionKeyName = dynamoTableProps.partitionKey.name; + this.table = new dynamodb.Table(this, 'DynamoTable', dynamoTableProps); + } else { + partitionKeyName = defaults.DefaultTableProps.partitionKey.name; + this.table = new dynamodb.Table(this, 'DynamoTable', defaults.DefaultTableProps); + } + + // Setup the API Gateway + this.apiGateway = defaults.GlobalRestApi(this); + + // Setup the API Gateway role + this.apiGatewayRole = new iam.Role(this, 'api-gateway-role', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com') + }); + + // Setup the API Gateway Resource + const apiGatewayResource: api.Resource = this.apiGateway.root.addResource("{" + partitionKeyName + "}"); + + // Setup API Gateway Method + // Create + if (props.allowCreateOperation && props.allowCreateOperation === true && props.createRequestTemplate) { + const createRequestTemplate = props.createRequestTemplate.replace("${Table}", this.table.tableName); + this.addActiontoPlicy("dynamodb:PutItem"); + this.addMethod(this.apiGateway.root, createRequestTemplate, "PutItem", "POST"); + } + // Read + if (!props.allowReadOperation || props.allowReadOperation === true) { + const getRequestTemplate = "{\r\n\"TableName\": \"" + this.table.tableName + "\",\r\n \"KeyConditionExpression\": \"" + partitionKeyName + " = :v1\",\r\n \"ExpressionAttributeValues\": {\r\n \":v1\": {\r\n \"S\": \"$input.params('" + partitionKeyName + "')\"\r\n }\r\n }\r\n}"; + this.addActiontoPlicy("dynamodb:Query"); + this.addMethod(apiGatewayResource, getRequestTemplate, "Query", "GET"); + } + // Update + if (props.allowUpdateOperation && props.allowUpdateOperation === true && props.updateRequestTemplate) { + const updateRequestTemplate = props.updateRequestTemplate.replace("${Table}", this.table.tableName); + this.addActiontoPlicy("dynamodb:UpdateItem"); + this.addMethod(apiGatewayResource, updateRequestTemplate, "UpdateItem", "PUT"); + } + // Delete + if (props.allowDeleteOperation && props.allowDeleteOperation === true) { + const deleteRequestTemplate = "{\r\n \"TableName\": \"" + this.table.tableName + "\",\r\n \"Key\": {\r\n \"" + partitionKeyName + "\": {\r\n \"S\": \"$input.params('" + partitionKeyName + "')\"\r\n }\r\n },\r\n \"ConditionExpression\": \"attribute_not_exists(Replies)\",\r\n \"ReturnValues\": \"ALL_OLD\"\r\n}"; + this.addActiontoPlicy("dynamodb:DeleteItem"); + this.addMethod(apiGatewayResource, deleteRequestTemplate, "DeleteItem", "DELETE"); + } + } + + private addActiontoPlicy(action: string) { + this.apiGatewayRole.addToPolicy(new iam.PolicyStatement({ + resources: [ + this.table.tableArn + ], + actions: [ `${action}` ] + })); + } + + private addMethod(apiResource: api.IResource, requestTemplate: string, dynamodbAction: string, apiMethod: string) { + // Setup the API Gateway Integration + const apiGatewayIntegration = new api.AwsIntegration({ + service: "dynamodb", + action: dynamodbAction, + integrationHttpMethod: "POST", + options: { + passthroughBehavior: api.PassthroughBehavior.NEVER, + credentialsRole: this.apiGatewayRole, + requestParameters: { + "integration.request.header.Content-Type": "'application/json'" + }, + requestTemplates: { + "application/json": requestTemplate + }, + integrationResponses: [ + { + statusCode: "200" + }, + { + statusCode: "500", + responseTemplates: { + "text/html": "Error" + }, + selectionPattern: "500" + } + ] + } + }); + + // Setup the API Gateway method(s) + apiResource.addMethod(apiMethod, apiGatewayIntegration, { + methodResponses: [ + { + statusCode: "200", + responseParameters: { + "method.response.header.Content-Type": true + } + }, + { + statusCode: "500", + responseParameters: { + "method.response.header.Content-Type": true + }, + } + ] + }); + } + + /** + * @summary Returns an instance of the api.RestApi created by the construct. + * @returns {api.RestApi} Instance of the RestApi created by the construct. + * @since 0.8.0 + * @access public + */ + public restApi(): api.RestApi { + return this.apiGateway; + } + + /** + * @summary Retruns an instance of dynamodb.Table created by the construct. + * @returns {dynamodb.Table} Instance of dynamodb.Table created by the construct + * @since 0.8.0 + * @access public + */ + public dynamoTable(): dynamodb.Table { + return this.table; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/package.json b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/package.json new file mode 100644 index 000000000..7a1f6de41 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/package.json @@ -0,0 +1,79 @@ +{ + "name": "@aws-solutions-konstruk/aws-apigateway-dynamodb", + "version": "0.8.0", + "description": "CDK Constructs for AWS API Gateway and Amazon DynamoDB integration.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.apigatewaydynamodb", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "apigatewaydynamodb" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.ApiGatewayDynamoDB", + "packageId": "Amazon.Konstruk.AWS.ApiGatewayDynamoDB", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-apigateway-dynamodb", + "module": "aws_solutions_konstruk.aws_apigateway_dynamodb" + } + } + }, + "dependencies": { + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-dynamodb": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-dynamodb": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/__snapshots__/apigateway-dynamodb.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/__snapshots__/apigateway-dynamodb.test.js.snap new file mode 100644 index 000000000..9cde02214 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/__snapshots__/apigateway-dynamodb.test.js.snap @@ -0,0 +1,361 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test ApiGatewayToDynamoDB default params 1`] = ` +Object { + "Outputs": Object { + "testapigatewaydynamodbdefaultRestApiEndpointD5AD8DB9": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "testapigatewaydynamodbdefaultRestApi9102FDF9", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "testapigatewaydynamodbdefaultRestApiDeploymentStageprod7834D304", + }, + "/", + ], + ], + }, + }, + }, + "Resources": Object { + "testapigatewaydynamodbdefaultApiAccessLogGroup0192183A": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "testapigatewaydynamodbdefaultDynamoTable0720D92C": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "AttributeDefinitions": Array [ + Object { + "AttributeName": "id", + "AttributeType": "S", + }, + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": Array [ + Object { + "AttributeName": "id", + "KeyType": "HASH", + }, + ], + "SSESpecification": Object { + "SSEEnabled": true, + }, + }, + "Type": "AWS::DynamoDB::Table", + "UpdateReplacePolicy": "Retain", + }, + "testapigatewaydynamodbdefaultLambdaRestApiAccountE6585EBB": Object { + "DependsOn": Array [ + "testapigatewaydynamodbdefaultRestApi9102FDF9", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "testapigatewaydynamodbdefaultLambdaRestApiCloudWatchRoleEF1FBFD7", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "testapigatewaydynamodbdefaultLambdaRestApiCloudWatchRoleEF1FBFD7": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "testapigatewaydynamodbdefaultRestApi9102FDF9": Object { + "Properties": Object { + "EndpointConfiguration": Object { + "Types": Array [ + "EDGE", + ], + }, + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "testapigatewaydynamodbdefaultRestApiDeploymentFAC726F377bb1a1fb193e128da423ded28aa899d": Object { + "DependsOn": Array [ + "testapigatewaydynamodbdefaultRestApiidGET94B6F433", + "testapigatewaydynamodbdefaultRestApiidFD6A9E91", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "testapigatewaydynamodbdefaultRestApi9102FDF9", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "testapigatewaydynamodbdefaultRestApiDeploymentStageprod7834D304": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "testapigatewaydynamodbdefaultApiAccessLogGroup0192183A", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "testapigatewaydynamodbdefaultRestApiDeploymentFAC726F377bb1a1fb193e128da423ded28aa899d", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "testapigatewaydynamodbdefaultRestApi9102FDF9", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "testapigatewaydynamodbdefaultRestApiUsagePlanA266BB3D": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "testapigatewaydynamodbdefaultRestApi9102FDF9", + }, + "Stage": Object { + "Ref": "testapigatewaydynamodbdefaultRestApiDeploymentStageprod7834D304", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "testapigatewaydynamodbdefaultRestApiidFD6A9E91": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "testapigatewaydynamodbdefaultRestApi9102FDF9", + "RootResourceId", + ], + }, + "PathPart": "{id}", + "RestApiId": Object { + "Ref": "testapigatewaydynamodbdefaultRestApi9102FDF9", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "testapigatewaydynamodbdefaultRestApiidGET94B6F433": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "GET", + "Integration": Object { + "Credentials": Object { + "Fn::GetAtt": Array [ + "testapigatewaydynamodbdefaultapigatewayrole0CDF008A", + "Arn", + ], + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": Array [ + Object { + "StatusCode": "200", + }, + Object { + "ResponseTemplates": Object { + "text/html": "Error", + }, + "SelectionPattern": "500", + "StatusCode": "500", + }, + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": Object { + "integration.request.header.Content-Type": "'application/json'", + }, + "RequestTemplates": Object { + "application/json": Object { + "Fn::Join": Array [ + "", + Array [ + "{ +\\"TableName\\": \\"", + Object { + "Ref": "testapigatewaydynamodbdefaultDynamoTable0720D92C", + }, + "\\", + \\"KeyConditionExpression\\": \\"id = :v1\\", + \\"ExpressionAttributeValues\\": { + \\":v1\\": { + \\"S\\": \\"$input.params('id')\\" + } + } +}", + ], + ], + }, + }, + "Type": "AWS", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":dynamodb:action/Query", + ], + ], + }, + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "200", + }, + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "500", + }, + ], + "ResourceId": Object { + "Ref": "testapigatewaydynamodbdefaultRestApiidFD6A9E91", + }, + "RestApiId": Object { + "Ref": "testapigatewaydynamodbdefaultRestApi9102FDF9", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "testapigatewaydynamodbdefaultapigatewayrole0CDF008A": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "testapigatewaydynamodbdefaultapigatewayroleDefaultPolicyE0B5E59D": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "dynamodb:Query", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "testapigatewaydynamodbdefaultDynamoTable0720D92C", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "testapigatewaydynamodbdefaultapigatewayroleDefaultPolicyE0B5E59D", + "Roles": Array [ + Object { + "Ref": "testapigatewaydynamodbdefaultapigatewayrole0CDF008A", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts new file mode 100644 index 000000000..7a01ccad7 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/apigateway-dynamodb.test.ts @@ -0,0 +1,192 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { Stack } from "@aws-cdk/core"; +import { ApiGatewayToDynamoDB, ApiGatewayToDynamoDBProps } from '../lib'; +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { AttributeType } from "@aws-cdk/aws-dynamodb"; + +test('snapshot test ApiGatewayToDynamoDB default params', () => { + const stack = new Stack(); + const apiGatewayToDynamoDBProps: ApiGatewayToDynamoDBProps = {}; + new ApiGatewayToDynamoDB(stack, 'test-api-gateway-dynamodb-default', apiGatewayToDynamoDBProps); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check getter methods', () => { + const stack = new Stack(); + const apiGatewayToDynamoDBProps: ApiGatewayToDynamoDBProps = {}; + const construct = new ApiGatewayToDynamoDB(stack, 'test-api-gateway-dynamodb-default', apiGatewayToDynamoDBProps); + + expect(construct.dynamoTable()).toBeDefined(); + expect(construct.restApi()).toBeDefined(); +}); + +test('check allow CRUD operations', () => { + const stack = new Stack(); + const apiGatewayToDynamoDBProps: ApiGatewayToDynamoDBProps = { + allowReadOperation: true, + allowCreateOperation: true, + createRequestTemplate: "{}", + allowDeleteOperation: true, + allowUpdateOperation: true, + updateRequestTemplate: "{}" + }; + new ApiGatewayToDynamoDB(stack, 'test-api-gateway-dynamodb', apiGatewayToDynamoDBProps); + + expect(stack).toHaveResource("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: "dynamodb:PutItem", + Effect: "Allow", + Resource: { + "Fn::GetAtt": [ + "testapigatewaydynamodbDynamoTableEEE3F463", + "Arn" + ] + } + }, + { + Action: "dynamodb:Query", + Effect: "Allow", + Resource: { + "Fn::GetAtt": [ + "testapigatewaydynamodbDynamoTableEEE3F463", + "Arn" + ] + } + }, + { + Action: "dynamodb:UpdateItem", + Effect: "Allow", + Resource: { + "Fn::GetAtt": [ + "testapigatewaydynamodbDynamoTableEEE3F463", + "Arn" + ] + } + }, + { + Action: "dynamodb:DeleteItem", + Effect: "Allow", + Resource: { + "Fn::GetAtt": [ + "testapigatewaydynamodbDynamoTableEEE3F463", + "Arn" + ] + } + } + ], + Version: "2012-10-17" + }, + PolicyName: "testapigatewaydynamodbapigatewayroleDefaultPolicy43AC565D", + Roles: [ + { + Ref: "testapigatewaydynamodbapigatewayrole961B19C4" + } + ] + }); + + expect(stack).toHaveResource("AWS::ApiGateway::Method", { + HttpMethod: "GET", + AuthorizationType: "AWS_IAM" + }); + + expect(stack).toHaveResource("AWS::ApiGateway::Method", { + HttpMethod: "POST", + AuthorizationType: "AWS_IAM" + }); + + expect(stack).toHaveResource("AWS::ApiGateway::Method", { + HttpMethod: "PUT", + AuthorizationType: "AWS_IAM" + }); + + expect(stack).toHaveResource("AWS::ApiGateway::Method", { + HttpMethod: "DELETE", + AuthorizationType: "AWS_IAM" + }); + + expect(stack).toHaveResource("AWS::ApiGateway::Resource", { + PathPart: "{id}", + }); +}); + +test('check allow read and update only', () => { + const stack = new Stack(); + const apiGatewayToDynamoDBProps: ApiGatewayToDynamoDBProps = { + allowUpdateOperation: true, + updateRequestTemplate: "{}" + }; + new ApiGatewayToDynamoDB(stack, 'test-api-gateway-dynamodb', apiGatewayToDynamoDBProps); + + expect(stack).toHaveResource("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: "dynamodb:Query", + Effect: "Allow", + Resource: { + "Fn::GetAtt": [ + "testapigatewaydynamodbDynamoTableEEE3F463", + "Arn" + ] + } + }, + { + Action: "dynamodb:UpdateItem", + Effect: "Allow", + Resource: { + "Fn::GetAtt": [ + "testapigatewaydynamodbDynamoTableEEE3F463", + "Arn" + ] + } + } + ], + Version: "2012-10-17" + }, + PolicyName: "testapigatewaydynamodbapigatewayroleDefaultPolicy43AC565D", + Roles: [ + { + Ref: "testapigatewaydynamodbapigatewayrole961B19C4" + } + ] + }); + + expect(stack).toHaveResource("AWS::ApiGateway::Method", { + HttpMethod: "GET", + AuthorizationType: "AWS_IAM" + }); +}); + +test('check using custom partition key for dynamodb', () => { + const stack = new Stack(); + const apiGatewayToDynamoDBProps: ApiGatewayToDynamoDBProps = { + dynamoTableProps: { + partitionKey: { + name: 'page_id', + type: AttributeType.STRING + } + } + }; + new ApiGatewayToDynamoDB(stack, 'test-api-gateway-dynamodb', apiGatewayToDynamoDBProps); + + expect(stack).toHaveResource("AWS::ApiGateway::Resource", { + PathPart: "{page_id}", + }); + +}); diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/integ.apigateway-dynamodb-CRUD.expected.json b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/integ.apigateway-dynamodb-CRUD.expected.json new file mode 100644 index 000000000..ac65b24ea --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/integ.apigateway-dynamodb-CRUD.expected.json @@ -0,0 +1,635 @@ +{ + "Description": "Integration Test for aws-apigateway-dynamodb", + "Resources": { + "testapigatewaydynamodbDynamoTableEEE3F463": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testapigatewaydynamodbRestApi80489300": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "testapigatewaydynamodbRestApiDeployment1898674B2ca5fe4ea0afc42b42d1b0c68ddb65ce": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaydynamodbRestApi80489300" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "testapigatewaydynamodbRestApiidDELETE6FC8A9F3", + "testapigatewaydynamodbRestApiidGET6196F638", + "testapigatewaydynamodbRestApiidPUT1F965B23", + "testapigatewaydynamodbRestApiid78018D34", + "testapigatewaydynamodbRestApiPOSTE99BD0BD" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "testapigatewaydynamodbRestApiDeploymentStageprod2855C2C3": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaydynamodbRestApi80489300" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testapigatewaydynamodbApiAccessLogGroup3F457756", + "Arn" + ] + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + }, + "DeploymentId": { + "Ref": "testapigatewaydynamodbRestApiDeployment1898674B2ca5fe4ea0afc42b42d1b0c68ddb65ce" + }, + "MethodSettings": [ + { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod" + } + }, + "testapigatewaydynamodbRestApiid78018D34": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testapigatewaydynamodbRestApi80489300", + "RootResourceId" + ] + }, + "PathPart": "{id}", + "RestApiId": { + "Ref": "testapigatewaydynamodbRestApi80489300" + } + } + }, + "testapigatewaydynamodbRestApiidGET6196F638": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Ref": "testapigatewaydynamodbRestApiid78018D34" + }, + "RestApiId": { + "Ref": "testapigatewaydynamodbRestApi80489300" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaydynamodbapigatewayrole961B19C4", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/json'" + }, + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "{\r\n\"TableName\": \"", + { + "Ref": "testapigatewaydynamodbDynamoTableEEE3F463" + }, + "\",\r\n \"KeyConditionExpression\": \"id = :v1\",\r\n \"ExpressionAttributeValues\": {\r\n \":v1\": {\r\n \"S\": \"$input.params('id')\"\r\n }\r\n }\r\n}" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":dynamodb:action/Query" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaydynamodbRestApiidPUT1F965B23": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "PUT", + "ResourceId": { + "Ref": "testapigatewaydynamodbRestApiid78018D34" + }, + "RestApiId": { + "Ref": "testapigatewaydynamodbRestApi80489300" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaydynamodbapigatewayrole961B19C4", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/json'" + }, + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "{\r\n \"TableName\": \"", + { + "Ref": "testapigatewaydynamodbDynamoTableEEE3F463" + }, + "\",\r\n \"Key\": {\r\n \"id\": {\r\n \"S\": \"$input.path('$.id')\"\r\n }\r\n },\r\n \"ExpressionAttributeValues\": {\r\n \":event_count\": {\r\n \"N\": \"$input.path('$.EventCount')\"\r\n },\r\n \":message\": {\r\n \"S\": \"$input.path('$.Message')\"\r\n }\r\n },\r\n \"UpdateExpression\": \"ADD EventCount :event_count SET Message = :message\",\r\n \"ReturnValues\": \"ALL_NEW\"\r\n}" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":dynamodb:action/UpdateItem" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaydynamodbRestApiidDELETE6FC8A9F3": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "DELETE", + "ResourceId": { + "Ref": "testapigatewaydynamodbRestApiid78018D34" + }, + "RestApiId": { + "Ref": "testapigatewaydynamodbRestApi80489300" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaydynamodbapigatewayrole961B19C4", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/json'" + }, + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "{\r\n \"TableName\": \"", + { + "Ref": "testapigatewaydynamodbDynamoTableEEE3F463" + }, + "\",\r\n \"Key\": {\r\n \"id\": {\r\n \"S\": \"$input.params('id')\"\r\n }\r\n },\r\n \"ConditionExpression\": \"attribute_not_exists(Replies)\",\r\n \"ReturnValues\": \"ALL_OLD\"\r\n}" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":dynamodb:action/DeleteItem" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaydynamodbRestApiPOSTE99BD0BD": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "ResourceId": { + "Fn::GetAtt": [ + "testapigatewaydynamodbRestApi80489300", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "testapigatewaydynamodbRestApi80489300" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaydynamodbapigatewayrole961B19C4", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/json'" + }, + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "{\r\n \"TableName\": \"", + { + "Ref": "testapigatewaydynamodbDynamoTableEEE3F463" + }, + "\",\r\n \"Item\": {\r\n \"id\": {\r\n \"S\": \"$input.path('$.id')\"\r\n },\r\n \"EventCount\": {\r\n \"N\": \"$input.path('$.EventCount')\"\r\n },\r\n \"Message\": {\r\n \"S\": \"$input.path('$.Message')\"\r\n }\r\n }\r\n}" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":dynamodb:action/PutItem" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaydynamodbRestApiUsagePlan244F06C8": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "testapigatewaydynamodbRestApi80489300" + }, + "Stage": { + "Ref": "testapigatewaydynamodbRestApiDeploymentStageprod2855C2C3" + }, + "Throttle": {} + } + ] + } + }, + "testapigatewaydynamodbApiAccessLogGroup3F457756": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testapigatewaydynamodbLambdaRestApiCloudWatchRoleD176CA9E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "testapigatewaydynamodbLambdaRestApiAccount3608999D": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "testapigatewaydynamodbLambdaRestApiCloudWatchRoleD176CA9E", + "Arn" + ] + } + }, + "DependsOn": [ + "testapigatewaydynamodbRestApi80489300" + ] + }, + "testapigatewaydynamodbapigatewayrole961B19C4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testapigatewaydynamodbapigatewayroleDefaultPolicy43AC565D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:PutItem", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testapigatewaydynamodbDynamoTableEEE3F463", + "Arn" + ] + } + }, + { + "Action": "dynamodb:Query", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testapigatewaydynamodbDynamoTableEEE3F463", + "Arn" + ] + } + }, + { + "Action": "dynamodb:UpdateItem", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testapigatewaydynamodbDynamoTableEEE3F463", + "Arn" + ] + } + }, + { + "Action": "dynamodb:DeleteItem", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testapigatewaydynamodbDynamoTableEEE3F463", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testapigatewaydynamodbapigatewayroleDefaultPolicy43AC565D", + "Roles": [ + { + "Ref": "testapigatewaydynamodbapigatewayrole961B19C4" + } + ] + } + } + }, + "Outputs": { + "testapigatewaydynamodbRestApiEndpoint18D89CA6": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testapigatewaydynamodbRestApi80489300" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testapigatewaydynamodbRestApiDeploymentStageprod2855C2C3" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/integ.apigateway-dynamodb-CRUD.ts b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/integ.apigateway-dynamodb-CRUD.ts new file mode 100644 index 000000000..15442846c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/integ.apigateway-dynamodb-CRUD.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { ApiGatewayToDynamoDBProps, ApiGatewayToDynamoDB } from "../lib"; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-apigateway-dynamodb'); +stack.templateOptions.description = 'Integration Test for aws-apigateway-dynamodb'; + +// Definitions +const props: ApiGatewayToDynamoDBProps = { + allowReadOperation: true, + allowCreateOperation: true, + allowDeleteOperation: true, + allowUpdateOperation: true, + createRequestTemplate: "{\r\n \"TableName\": \"${Table}\",\r\n \"Item\": {\r\n \"id\": {\r\n \"S\": \"$input.path('$.id')\"\r\n },\r\n \"EventCount\": {\r\n \"N\": \"$input.path('$.EventCount')\"\r\n },\r\n \"Message\": {\r\n \"S\": \"$input.path('$.Message')\"\r\n }\r\n }\r\n}", + updateRequestTemplate: "{\r\n \"TableName\": \"${Table}\",\r\n \"Key\": {\r\n \"id\": {\r\n \"S\": \"$input.path('$.id')\"\r\n }\r\n },\r\n \"ExpressionAttributeValues\": {\r\n \":event_count\": {\r\n \"N\": \"$input.path('$.EventCount')\"\r\n },\r\n \":message\": {\r\n \"S\": \"$input.path('$.Message')\"\r\n }\r\n },\r\n \"UpdateExpression\": \"ADD EventCount :event_count SET Message = :message\",\r\n \"ReturnValues\": \"ALL_NEW\"\r\n}" +}; + +new ApiGatewayToDynamoDB(stack, 'test-api-gateway-dynamodb', props); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..258a1c11d --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/integ.no-arguments.expected.json @@ -0,0 +1,350 @@ +{ + "Description": "Integration Test for aws-apigateway-dynamodb", + "Resources": { + "testapigatewaydynamodbdefaultDynamoTable0720D92C": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testapigatewaydynamodbdefaultRestApi9102FDF9": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "testapigatewaydynamodbdefaultRestApiDeploymentFAC726F377bb1a1fb193e128da423ded28aa899d": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaydynamodbdefaultRestApi9102FDF9" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "testapigatewaydynamodbdefaultRestApiidGET94B6F433", + "testapigatewaydynamodbdefaultRestApiidFD6A9E91" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "testapigatewaydynamodbdefaultRestApiDeploymentStageprod7834D304": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaydynamodbdefaultRestApi9102FDF9" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testapigatewaydynamodbdefaultApiAccessLogGroup0192183A", + "Arn" + ] + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + }, + "DeploymentId": { + "Ref": "testapigatewaydynamodbdefaultRestApiDeploymentFAC726F377bb1a1fb193e128da423ded28aa899d" + }, + "MethodSettings": [ + { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod" + } + }, + "testapigatewaydynamodbdefaultRestApiidFD6A9E91": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testapigatewaydynamodbdefaultRestApi9102FDF9", + "RootResourceId" + ] + }, + "PathPart": "{id}", + "RestApiId": { + "Ref": "testapigatewaydynamodbdefaultRestApi9102FDF9" + } + } + }, + "testapigatewaydynamodbdefaultRestApiidGET94B6F433": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Ref": "testapigatewaydynamodbdefaultRestApiidFD6A9E91" + }, + "RestApiId": { + "Ref": "testapigatewaydynamodbdefaultRestApi9102FDF9" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaydynamodbdefaultapigatewayrole0CDF008A", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/json'" + }, + "RequestTemplates": { + "application/json": { + "Fn::Join": [ + "", + [ + "{\r\n\"TableName\": \"", + { + "Ref": "testapigatewaydynamodbdefaultDynamoTable0720D92C" + }, + "\",\r\n \"KeyConditionExpression\": \"id = :v1\",\r\n \"ExpressionAttributeValues\": {\r\n \":v1\": {\r\n \"S\": \"$input.params('id')\"\r\n }\r\n }\r\n}" + ] + ] + } + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":dynamodb:action/Query" + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaydynamodbdefaultRestApiUsagePlanA266BB3D": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "testapigatewaydynamodbdefaultRestApi9102FDF9" + }, + "Stage": { + "Ref": "testapigatewaydynamodbdefaultRestApiDeploymentStageprod7834D304" + }, + "Throttle": {} + } + ] + } + }, + "testapigatewaydynamodbdefaultApiAccessLogGroup0192183A": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testapigatewaydynamodbdefaultLambdaRestApiCloudWatchRoleEF1FBFD7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "testapigatewaydynamodbdefaultLambdaRestApiAccountE6585EBB": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "testapigatewaydynamodbdefaultLambdaRestApiCloudWatchRoleEF1FBFD7", + "Arn" + ] + } + }, + "DependsOn": [ + "testapigatewaydynamodbdefaultRestApi9102FDF9" + ] + }, + "testapigatewaydynamodbdefaultapigatewayrole0CDF008A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testapigatewaydynamodbdefaultapigatewayroleDefaultPolicyE0B5E59D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:Query", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testapigatewaydynamodbdefaultDynamoTable0720D92C", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testapigatewaydynamodbdefaultapigatewayroleDefaultPolicyE0B5E59D", + "Roles": [ + { + "Ref": "testapigatewaydynamodbdefaultapigatewayrole0CDF008A" + } + ] + } + } + }, + "Outputs": { + "testapigatewaydynamodbdefaultRestApiEndpointD5AD8DB9": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testapigatewaydynamodbdefaultRestApi9102FDF9" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testapigatewaydynamodbdefaultRestApiDeploymentStageprod7834D304" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/integ.no-arguments.ts new file mode 100644 index 000000000..db6b9c799 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-dynamodb/test/integ.no-arguments.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { ApiGatewayToDynamoDBProps, ApiGatewayToDynamoDB } from "../lib"; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-apigateway-dynamodb-default'); +stack.templateOptions.description = 'Integration Test for aws-apigateway-dynamodb'; + +// Definitions +const props: ApiGatewayToDynamoDBProps = { +}; + +new ApiGatewayToDynamoDB(stack, 'test-api-gateway-dynamodb-default', props); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/README.md b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/README.md new file mode 100644 index 000000000..3de5d141c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/README.md @@ -0,0 +1,78 @@ +# aws-apigateway-lambda module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-apigateway-lambda/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_apigateway_lambda`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-apigateway-lambda`| + +## Overview + +This AWS Solutions Konstruk implements an Amazon API Gateway REST API connected to an AWS Lambda function pattern. + +Here is a minimal deployable pattern definition: + +``` javascript +const { ApiGatewayToLambda } = require('@aws-solutions-konstruk/aws-apigateway-lambda'); + +new ApiGatewayToLambda(stack, 'ApiGatewayToLambdaPattern', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } +}); + +``` + +## Initializer + +``` text +new ApiGatewayToLambda(scope: Construct, id: string, props: ApiGatewayToLambdaProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`ApiGatewayToLambdaProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function. If set to false, you must provide an existing function for the `existingLambdaObj` property.| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|An optional, existing Lambda function. This property is required if `deployLambda` is set to false.| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user-provided props to override the default props for the Lambda function. This property is only required if `deployLambda` is set to true.| +|apiGatewayProps?|[`api.LambdaRestApiProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.LambdaRestApi.html)|Optional user-provided props to override the default props for the API.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Returns an instance of the Lambda function created by the pattern.| +|restApi()|[`api.LambdaRestApi`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.LambdaRestApi.html)|Returns an instance of the API Gateway REST API created by the pattern.| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/architecture.png new file mode 100644 index 000000000..4e617ab6e Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/lib/index.ts new file mode 100644 index 000000000..34b852ae3 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/lib/index.ts @@ -0,0 +1,122 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import * as api from '@aws-cdk/aws-apigateway'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; +import { LogGroup } from '@aws-cdk/aws-logs'; + +/** + * The properties for the ApiGatewayToLambda class. + */ +export interface ApiGatewayToLambdaProps { + /** + * Whether to create a new Lambda function or use an existing Lambda function. + * If set to false, you must provide an existing function for the `existingLambdaObj` property. + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * An optional, existing Lambda function. + * This property is required if `deployLambda` is set to false. + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user-provided props to override the default props for the Lambda function. + * This property is only required if `deployLambda` is set to true. + * + * @default - Default props are used. + */ + readonly lambdaFunctionProps?: lambda.FunctionProps | any + /** + * Optional user-provided props to override the default props for the API. + * + * @default - Default props are used. + */ + readonly apiGatewayProps?: api.LambdaRestApiProps | any +} + +/** + * @summary The ApiGatewayToLambda class. + */ +export class ApiGatewayToLambda extends Construct { + // Private variables + private api: api.RestApi; + private fn: lambda.Function; + + /** + * @summary Constructs a new instance of the ApiGatewayToLambda class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {CloudFrontToApiGatewayToLambdaProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: ApiGatewayToLambdaProps) { + super(scope, id); + + // Setup the Lambda function + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + + // Setup the API Gateway + this.api = defaults.GlobalLambdaRestApi(scope, this.fn, props.apiGatewayProps); + + // Setup the log group + const logGroup = new LogGroup(this, 'ApiAccessLogGroup'); + + // Configure API Gateway Access logging + const stage: api.CfnStage = this.api.deploymentStage.node.findChild('Resource') as api.CfnStage; + stage.accessLogSetting = { + destinationArn: logGroup.logGroupArn, + format: "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + }; + const deployment: api.CfnDeployment = this.api.latestDeployment?.node.findChild('Resource') as api.CfnDeployment; + deployment.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W45', + reason: `ApiGateway does have access logging configured as part of AWS::ApiGateway::Stage.` + }] + } + }; + } + + /** + * @summary Returns an instance of lambda.Function created by the construct. + * @returns { lambda.Function } Instance of Function created by the construct + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } + + /** + * @summary Returns an instance of api.LambdaRestApi created by the construct. + * @returns { api.LambdaRestApi } Instance of LambdaRestApi created by the construct + * @since 0.8.0 + * @access public + */ + public restApi(): api.LambdaRestApi { + return this.api; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/package.json b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/package.json new file mode 100644 index 000000000..10e5292a6 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/package.json @@ -0,0 +1,79 @@ +{ + "name": "@aws-solutions-konstruk/aws-apigateway-lambda", + "version": "0.8.0", + "description": "CDK constructs for defining an interaction between an API Gateway and a Lambda function.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.apigatewaylambda", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "apigatewaylambda" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.ApiGatewayLambda", + "packageId": "Amazon.Konstruk.AWS.ApiGatewayLambda", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-apigateway-lambda", + "module": "aws_solutions_konstruk.aws_apigateway_lambda" + } + } + }, + "dependencies": { + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-logs": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-logs": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/__snapshots__/test.apigateway-lambda.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/__snapshots__/test.apigateway-lambda.test.js.snap new file mode 100644 index 000000000..23318957b --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/__snapshots__/test.apigateway-lambda.test.js.snap @@ -0,0 +1,1136 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pattern deployment with existing Lambda function 1`] = ` +Object { + "Outputs": Object { + "RestApiEndpoint0551178A": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "RestApi0C43BF4B", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "ApiAccessLogGroupCEA70788": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "ExistingLambdaFunctionF606C520": Object { + "DependsOn": Array [ + "ExistingLambdaFunctionServiceRole7CC6DE65", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "ExistingLambdaFunctionServiceRole7CC6DE65", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "ExistingLambdaFunctionServiceRole7CC6DE65": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaRestApiAccount": Object { + "DependsOn": Array [ + "RestApi0C43BF4B", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "LambdaRestApiCloudWatchRoleF339D4E6": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "RestApi0C43BF4B": Object { + "Properties": Object { + "EndpointConfiguration": Object { + "Types": Array [ + "EDGE", + ], + }, + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "RestApiANYA7C1DC94": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "ExistingLambdaFunctionF606C520", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiANYApiPermissionRestApiANY3A99B4EE": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "ExistingLambdaFunctionF606C520", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiANYApiPermissionTestRestApiANY79BD91F2": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "ExistingLambdaFunctionF606C520", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiDeployment180EC503e06cd95e76bb0f117b881703f487bbf5": Object { + "DependsOn": Array [ + "RestApiproxyANY1786B242", + "RestApiproxyC95856DD", + "RestApiANYA7C1DC94", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway does have access logging configured as part of AWS::ApiGateway::Stage.", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "RestApiDeploymentStageprod3855DE66": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "testapigatewaylambdaApiAccessLogGroupEB3253A2", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "RestApiDeployment180EC503e06cd95e76bb0f117b881703f487bbf5", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "RestApiUsagePlan6E1C537A": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "Stage": Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "RestApiproxyANY1786B242": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "ExistingLambdaFunctionF606C520", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "RestApiproxyC95856DD", + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiproxyANYApiPermissionRestApiANYproxy9C9912F9": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "ExistingLambdaFunctionF606C520", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyANYApiPermissionTestRestApiANYproxyCB7BC56D": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "ExistingLambdaFunctionF606C520", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyC95856DD": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "testapigatewaylambdaApiAccessLogGroupEB3253A2": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "RetentionInDays": 731, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; + +exports[`Pattern deployment with new Lambda function 1`] = ` +Object { + "Outputs": Object { + "RestApiEndpoint0551178A": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "RestApi0C43BF4B", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "ApiAccessLogGroupCEA70788": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaRestApiAccount": Object { + "DependsOn": Array [ + "RestApi0C43BF4B", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "LambdaRestApiCloudWatchRoleF339D4E6": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "RestApi0C43BF4B": Object { + "Properties": Object { + "EndpointConfiguration": Object { + "Types": Array [ + "EDGE", + ], + }, + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "RestApiANYA7C1DC94": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiANYApiPermissionRestApiANY3A99B4EE": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiANYApiPermissionTestRestApiANY79BD91F2": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8": Object { + "DependsOn": Array [ + "RestApiproxyANY1786B242", + "RestApiproxyC95856DD", + "RestApiANYA7C1DC94", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway does have access logging configured as part of AWS::ApiGateway::Stage.", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "RestApiDeploymentStageprod3855DE66": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "testapigatewaylambdaApiAccessLogGroupEB3253A2", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "RestApiUsagePlan6E1C537A": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "Stage": Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "RestApiproxyANY1786B242": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "RestApiproxyC95856DD", + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiproxyANYApiPermissionRestApiANYproxy9C9912F9": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyANYApiPermissionTestRestApiANYproxyCB7BC56D": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyC95856DD": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "testapigatewaylambdaApiAccessLogGroupEB3253A2": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "RetentionInDays": 731, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/integ.deployFunction.expected.json b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/integ.deployFunction.expected.json new file mode 100644 index 000000000..90ccde175 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/integ.deployFunction.expected.json @@ -0,0 +1,583 @@ +{ + "Description": "Integration Test for aws-apigateway-lambda", + "Resources": { + "testapigatewaylambdaApiAccessLogGroupEB3253A2": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "RestApi0C43BF4B": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "RestApiproxyANY1786B242", + "RestApiproxyC95856DD", + "RestApiANYA7C1DC94" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway does have access logging configured as part of AWS::ApiGateway::Stage." + } + ] + } + } + }, + "RestApiDeploymentStageprod3855DE66": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testapigatewaylambdaApiAccessLogGroupEB3253A2", + "Arn" + ] + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + }, + "DeploymentId": { + "Ref": "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8" + }, + "MethodSettings": [ + { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod" + } + }, + "RestApiproxyC95856DD": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "PathPart": "{proxy+}", + "RestApiId": { + "Ref": "RestApi0C43BF4B" + } + } + }, + "RestApiproxyANYApiPermissiontestapigatewaylambdaRestApi54300087ANYproxy2DB4C4FC": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/*/{proxy+}" + ] + ] + } + } + }, + "RestApiproxyANYApiPermissionTesttestapigatewaylambdaRestApi54300087ANYproxy9A552081": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/test-invoke-stage/*/{proxy+}" + ] + ] + } + } + }, + "RestApiproxyANY1786B242": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Ref": "RestApiproxyC95856DD" + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "RestApiANYApiPermissiontestapigatewaylambdaRestApi54300087ANY87811EBD": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/*/" + ] + ] + } + } + }, + "RestApiANYApiPermissionTesttestapigatewaylambdaRestApi54300087ANY855753FC": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/test-invoke-stage/*/" + ] + ] + } + } + }, + "RestApiANYA7C1DC94": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "RestApiUsagePlan6E1C537A": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "RestApi0C43BF4B" + }, + "Stage": { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "Throttle": {} + } + ] + } + }, + "ApiAccessLogGroupCEA70788": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LambdaRestApiCloudWatchRoleF339D4E6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "LambdaRestApiAccount": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn" + ] + } + }, + "DependsOn": [ + "RestApi0C43BF4B" + ] + } + }, + "Parameters": { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": { + "Type": "String", + "Description": "S3 bucket for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": { + "Type": "String", + "Description": "S3 key for asset version \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": { + "Type": "String", + "Description": "Artifact hash for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + } + }, + "Outputs": { + "RestApiEndpoint0551178A": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "RestApi0C43BF4B" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/integ.deployFunction.ts b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/integ.deployFunction.ts new file mode 100644 index 000000000..5248dd78e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/integ.deployFunction.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { ApiGatewayToLambda, ApiGatewayToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-apigateway-lambda'); +stack.templateOptions.description = 'Integration Test for aws-apigateway-lambda'; + +// Definitions +const props: ApiGatewayToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } +}; + +new ApiGatewayToLambda(stack, 'test-apigateway-lambda', props); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/integ.existingFunction.expected.json b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/integ.existingFunction.expected.json new file mode 100644 index 000000000..8cd444c91 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/integ.existingFunction.expected.json @@ -0,0 +1,583 @@ +{ + "Description": "Integration Test for aws-apigateway-lambda", + "Resources": { + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "testapigatewaylambdaApiAccessLogGroupEB3253A2": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "RestApi0C43BF4B": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "RestApiproxyANY1786B242", + "RestApiproxyC95856DD", + "RestApiANYA7C1DC94" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway does have access logging configured as part of AWS::ApiGateway::Stage." + } + ] + } + } + }, + "RestApiDeploymentStageprod3855DE66": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testapigatewaylambdaApiAccessLogGroupEB3253A2", + "Arn" + ] + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + }, + "DeploymentId": { + "Ref": "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8" + }, + "MethodSettings": [ + { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod" + } + }, + "RestApiproxyC95856DD": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "PathPart": "{proxy+}", + "RestApiId": { + "Ref": "RestApi0C43BF4B" + } + } + }, + "RestApiproxyANYApiPermissiontestapigatewaylambdaRestApi54300087ANYproxy2DB4C4FC": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/*/{proxy+}" + ] + ] + } + } + }, + "RestApiproxyANYApiPermissionTesttestapigatewaylambdaRestApi54300087ANYproxy9A552081": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/test-invoke-stage/*/{proxy+}" + ] + ] + } + } + }, + "RestApiproxyANY1786B242": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Ref": "RestApiproxyC95856DD" + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "RestApiANYApiPermissiontestapigatewaylambdaRestApi54300087ANY87811EBD": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/*/" + ] + ] + } + } + }, + "RestApiANYApiPermissionTesttestapigatewaylambdaRestApi54300087ANY855753FC": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/test-invoke-stage/*/" + ] + ] + } + } + }, + "RestApiANYA7C1DC94": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "RestApiUsagePlan6E1C537A": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "RestApi0C43BF4B" + }, + "Stage": { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "Throttle": {} + } + ] + } + }, + "ApiAccessLogGroupCEA70788": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LambdaRestApiCloudWatchRoleF339D4E6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "LambdaRestApiAccount": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn" + ] + } + }, + "DependsOn": [ + "RestApi0C43BF4B" + ] + } + }, + "Parameters": { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": { + "Type": "String", + "Description": "S3 bucket for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": { + "Type": "String", + "Description": "S3 key for asset version \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": { + "Type": "String", + "Description": "Artifact hash for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + } + }, + "Outputs": { + "RestApiEndpoint0551178A": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "RestApi0C43BF4B" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/integ.existingFunction.ts b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/integ.existingFunction.ts new file mode 100644 index 000000000..5d311aa88 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/integ.existingFunction.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { ApiGatewayToLambda, ApiGatewayToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as defaults from '@aws-solutions-konstruk/core'; + +// App setup +const app = new App(); +const stack = new Stack(app, 'test-apigateway-lambda'); +stack.templateOptions.description = 'Integration Test for aws-apigateway-lambda'; + +// Lambda function setup +const lambdaFunctionProps = { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) +}; + +const func = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + +// Api gateway setup +const props: ApiGatewayToLambdaProps = { + deployLambda: false, + existingLambdaObj: func +}; + +// Instantiate construct +new ApiGatewayToLambda(stack, 'test-apigateway-lambda', props); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/lambda/index.js new file mode 100644 index 000000000..51fdc6953 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/lambda/index.js @@ -0,0 +1,21 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +exports.handler = async (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); +    return { +      statusCode: 200, +      headers: { 'Content-Type': 'text/plain' }, +      body: `Hello from Project Vesper! You've hit ${event.path}\n` +    }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/test.apigateway-lambda.test.ts b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/test.apigateway-lambda.test.ts new file mode 100644 index 000000000..c2e23ae31 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-lambda/test/test.apigateway-lambda.test.ts @@ -0,0 +1,138 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { Stack } from "@aws-cdk/core"; +import { ApiGatewayToLambda, ApiGatewayToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +// -------------------------------------------------------------- +// Pattern deployment with new Lambda function +// -------------------------------------------------------------- +test('Pattern deployment with new Lambda function', () => { + // Initial Setup + const stack = new Stack(); + const props: ApiGatewayToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } + }; + new ApiGatewayToLambda(stack, 'test-apigateway-lambda', props); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Pattern deployment with existing Lambda function +// -------------------------------------------------------------- +test('Pattern deployment with existing Lambda function', () => { + // Initial Setup + const stack = new Stack(); + const fn = new lambda.Function(stack, 'ExistingLambdaFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }); + const props: ApiGatewayToLambdaProps = { + deployLambda: false, + existingLambdaObj: fn + }; + new ApiGatewayToLambda(stack, 'test-apigateway-lambda', props); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test for error with deployLambda=false and +// existingLambdaObj=undefined (not supplied by user). +// -------------------------------------------------------------- +test('Error on deployLambda=false and existingLambdaObj=undefined', () => { + // Initial Setup + const stack = new Stack(); + const props: ApiGatewayToLambdaProps = { + deployLambda: false + }; + const app = () => { + new ApiGatewayToLambda(stack, 'test-apigateway-lambda', props); + }; + // Assertion 1 + expect(app).toThrowError(); +}); + +// -------------------------------------------------------------- +// Test deployLambda=true with lambdaFunctionProps. +// -------------------------------------------------------------- +test('Test deployLambda=true with lambdaFunctionProps', () => { + // Initial Setup + const stack = new Stack(); + const props: ApiGatewayToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`), + environment: { + OVERRIDE_STATUS: 'true' + } + }, + apiGatewayProps: { + description: "sampleApiProp" + } + }; + const app = new ApiGatewayToLambda(stack, 'test-apigateway-lambda', props); + // Assertion 1 + expect(app.lambdaFunction()).toHaveProperty('environment.OVERRIDE_STATUS', 'true'); +}); + +// -------------------------------------------------------------- +// Test getter methods +// -------------------------------------------------------------- +test('Test getter methods', () => { + // Initial Setup + const stack = new Stack(); + const props: ApiGatewayToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } + }; + const app = new ApiGatewayToLambda(stack, 'test-apigateway-lambda', props); + // Assertion 1 + expect(app.lambdaFunction()).toBeDefined(); + // Assertion 2 + expect(app.restApi()).toBeDefined(); +}); + +// -------------------------------------------------------------- +// Test for error with deployLambda=true and +// lambdaFunctionProps=undefined (not supplied by user). +// -------------------------------------------------------------- +test('Error on deployLambda=true and lambdaFunctionProps=undefined', () => { + // Initial Setup + const stack = new Stack(); + const props: ApiGatewayToLambdaProps = { + deployLambda: true + }; + const app = () => { + new ApiGatewayToLambda(stack, 'test-apigateway-lambda', props); + }; + // Assertion 1 + expect(app).toThrowError(); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/README.md b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/README.md new file mode 100644 index 000000000..cdfb9e4fd --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/README.md @@ -0,0 +1,86 @@ +# aws-apigateway-sqs module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-apigateway-sqs/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_apigateway_sqs`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-apigateway-sqs`| + +## Overview + +This AWS Solutions Konstruk implements an Amazon API Gateway connected to an Amazon SQS queue pattern. + +Here is a minimal deployable pattern definition: + +``` javascript +const { ApiGatewayToSqs } = require('@aws-solutions-konstruk/aws-apigateway-sqs'); + +new ApiGatewayToSqs(stack, 'ApiGatewayToSqsPattern', { + apiGatewayProps: {}, + queueProps: {}, + encryptionKeyProps: {}, + deployDeadLetterQueue?: true, + maxReceiveCount?: 3 +}); + +``` + +## Initializer + +``` text +new ApiGatewayToSqs(scope: Construct, id: string, props: ApiGatewayToSqsProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`ApiGatewayToSqsProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|apiGatewayProps?|[`api.RestApiProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.RestApiProps.html)|Optional user-provided props to override the default props for the API Gateway.| +|queueProps?|[`sqs.QueueProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sqs.QueueProps.html)|Optional user-provided props to override the default props for the queue.| +|encryptionKeyProps?|[`kms.KeyProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kms.KeyProps.html)|Optional user-provided props to override the default props for the encryption key.| +|deployDeadLetterQueue|`boolean`|Whether to deploy a secondary queue to be used as a dead letter queue.| +|maxReceiveCount|`number`|The number of times a message can be unsuccesfully dequeued before being moved to the dead-letter queue.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|api()|[`api.RestApi`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.RestApi.html)|Returns an instance of the API Gateway REST API created by the pattern.| +|sqsQueue()|[`sqs.Queue`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sqs.Queue.html)|Returns an instance of the SQS queue created by the pattern.| + +## Sample API Usage + +| **Method** | **Request Path** | **Request Body** | **Queue Action** | **Description** | +|:-------------|:----------------|-----------------|-----------------|-----------------| +|GET|`/`| |`sqs::ReceiveMessage`|Retrieves a message from the queue.| +|POST|`/`| `{ "data": "Hello World!" }` |`sqs::SendMessage`|Delivers a message to the queue.| +|DELETE|`/message?receiptHandle=[value]`||`sqs::DeleteMessage`|Deletes a specified message from the queue| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/architecture.png new file mode 100644 index 000000000..60eecaed4 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/lib/index.ts new file mode 100644 index 000000000..bac3b0681 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/lib/index.ts @@ -0,0 +1,240 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import * as api from '@aws-cdk/aws-apigateway'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as kms from '@aws-cdk/aws-kms'; +import * as iam from '@aws-cdk/aws-iam'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; +import * as cdk from '@aws-cdk/core'; + +/** + * @summary The properties for the ApiGatewayToSqs class. + */ +export interface ApiGatewayToSqsProps { + /** + * Optional user-provided props to override the default props for the API Gateway. + * + * @default - Default properties are used. + */ + readonly apiGatewayProps?: api.RestApiProps | any + /** + * Optional user-provided props to override the default props for the queue. + * + * @default - Default props are used + */ + readonly queueProps?: sqs.QueueProps | any + /** + * Optional user-provided props to override the default props for the encryption key. + * + * @default - Default props are used + */ + readonly encryptionKeyProps?: kms.KeyProps | any + /** + * Whether to deploy a secondary queue to be used as a dead letter queue. + * + * @default - required field. + */ + readonly deployDeadLetterQueue?: boolean, + /** + * The number of times a message can be unsuccesfully dequeued before being moved to the dead-letter queue. + * + * @default - required only if deployDeadLetterQueue = true. + */ + readonly maxReceiveCount?: number, + /** + * Whether to deploy an API Gateway Method for Create operations on the queue (i.e. sqs:SendMessage). + * + * @default - false + */ + readonly allowCreateOperation?: boolean, + /** + * API Gateway Request template for Create method, required if allowCreateOperation set to true + * + * @default - None + */ + readonly createRequestTemplate?: string, + /** + * Whether to deploy an API Gateway Method for Read operations on the queue (i.e. sqs:ReceiveMessage). + * + * @default - false + */ + readonly allowReadOperation?: boolean, + /** + * Whether to deploy an API Gateway Method for Delete operations on the queue (i.e. sqs:DeleteMessage). + * + * @default - false + */ + readonly allowDeleteOperation?: boolean +} + +/** + * @summary The ApiGatewayToSqs class. + */ +export class ApiGatewayToSqs extends Construct { + // Private variables + private encryptionKey: kms.Key; + private apiGateway: api.RestApi; + private apiGatewayRole: iam.Role; + private queue: sqs.Queue; + + /** + * @summary Constructs a new instance of the ApiGatewayToSqs class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {ApiGatewayToSqsProps} props - user provided props for the construct. + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: ApiGatewayToSqsProps) { + super(scope, id); + + // Setup the encryption key + this.encryptionKey = defaults.buildEncryptionKey(scope, props.encryptionKeyProps); + + // Setup the dead letter queue, if applicable + let dlqi: sqs.DeadLetterQueue | undefined; + if (!props.deployDeadLetterQueue || props.deployDeadLetterQueue === true) { + const dlq: sqs.Queue = defaults.buildQueue(scope, 'deadLetterQueue', { + encryptionKey: this.encryptionKey, + queueProps: props.queueProps + }); + dlqi = defaults.buildDeadLetterQueue({ + deadLetterQueue: dlq, + maxReceiveCount: (props.maxReceiveCount) ? props.maxReceiveCount : 3 + }); + } + + // Setup the queue + this.queue = defaults.buildQueue(scope, 'queue', { + encryptionKey: this.encryptionKey, + queueProps: props.queueProps, + deadLetterQueue: dlqi + }); + + // Setup the API Gateway + this.apiGateway = defaults.GlobalRestApi(this); + + // Setup the API Gateway role + this.apiGatewayRole = new iam.Role(this, 'api-gateway-role', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com') + }); + + // Setup the API Gateway resource + const apiGatewayResource = this.apiGateway.root.addResource('message'); + + // Grant encrypt/decrypt permissions for the API Gateway via KMS + this.encryptionKey.grantEncryptDecrypt(this.apiGatewayRole); + + // Setup API Gateway methods + // Create + if (props.allowCreateOperation && props.allowCreateOperation === true && props.createRequestTemplate) { + const createRequestTemplate = "Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")"; + this.addActionToPolicy("sqs:SendMessage"); + this.addMethod(this.apiGateway.root, createRequestTemplate, "POST"); + } + // Read + if (!props.allowReadOperation || props.allowReadOperation === true) { + const getRequestTemplate = "Action=ReceiveMessage"; + this.addActionToPolicy("sqs:ReceiveMessage"); + this.addMethod(this.apiGateway.root, getRequestTemplate, "GET"); + } + // Delete + if (props.allowDeleteOperation && props.allowDeleteOperation === true) { + const deleteRequestTemplate = "Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))"; + this.addActionToPolicy("sqs:DeleteMessage"); + this.addMethod(apiGatewayResource, deleteRequestTemplate, "DELETE"); + } + } + + private addActionToPolicy(action: string) { + this.apiGatewayRole.addToPolicy(new iam.PolicyStatement({ + resources: [ + this.queue.queueArn + ], + actions: [ `${action}` ] + })); + } + + private addMethod(apiResource: api.IResource, requestTemplate: string, apiMethod: string) { + // Add the integration + const apiGatewayIntegration = new api.AwsIntegration({ + service: "sqs", + path: `${cdk.Aws.ACCOUNT_ID}/${this.queue.queueName}`, + integrationHttpMethod: "POST", + options: { + passthroughBehavior: api.PassthroughBehavior.NEVER, + credentialsRole: this.apiGatewayRole, + requestParameters: { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" + }, + requestTemplates: { + "application/json": requestTemplate + }, + integrationResponses: [ + { + statusCode: "200" + }, + { + statusCode: "500", + responseTemplates: { + "text/html": "Error" + }, + selectionPattern: "500" + } + ] + } + }); + + // Add the method to the resource + apiResource.addMethod(apiMethod, apiGatewayIntegration, { + authorizationType: api.AuthorizationType.IAM, + methodResponses: [ + { + statusCode: "200", + responseParameters: { + "method.response.header.Content-Type": true + } + }, + { + statusCode: "500", + responseParameters: { + "method.response.header.Content-Type": true + }, + } + ] + }); + } + + /** + * @summary Returns an instance of the api.RestApi created by the construct. + * @returns {api.RestApi} Instance of the RestApi created by the construct. + * @since 0.8.0 + * @access public + */ + public api(): api.RestApi { + return this.apiGateway; + } + + /** + * @summary Returns an instance of the sqs.Queue created by the construct. + * @returns {sqs.Queue} Instance of the Queue created by the construct. + * @since 0.8.0 + * @access public + */ + public sqsQueue(): sqs.Queue { + return this.queue; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/package.json b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/package.json new file mode 100644 index 000000000..bc71d33cd --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/package.json @@ -0,0 +1,80 @@ +{ + "name": "@aws-solutions-konstruk/aws-apigateway-sqs", + "version": "0.8.0", + "description": "CDK constructs for defining an interaction between an AWS Lambda function and an Amazon S3 bucket.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.apigatewaysqs", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "apigatewaysqs" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.ApiGatewaySqs", + "packageId": "Amazon.Konstruk.AWS.ApiGatewaySqs", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-apigateway-sqs", + "module": "aws_solutions_konstruk.aws_apigateway_sqs" + } + } + }, + "dependencies": { + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-cdk/aws-sqs": "~1.25.0", + "@aws-cdk/aws-kms": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-cdk/aws-sqs": "~1.25.0", + "@aws-cdk/aws-kms": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/__snapshots__/apigateway-sqs.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/__snapshots__/apigateway-sqs.test.js.snap new file mode 100644 index 000000000..e1ea9320a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/__snapshots__/apigateway-sqs.test.js.snap @@ -0,0 +1,1720 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test deployment w/ DLQ 1`] = ` +Object { + "Outputs": Object { + "apigatewaysqsRestApiEndpointD55C9F0A": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "apigatewaysqsRestApiDeploymentStageprodAA3C7DD5", + }, + "/", + ], + ], + }, + }, + }, + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsapigatewayrole2BA120D3", + "Arn", + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "apigatewaysqsApiAccessLogGroup4D14D1D7": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "apigatewaysqsLambdaRestApiAccount8FA59342": Object { + "DependsOn": Array [ + "apigatewaysqsRestApi03BFD711", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsLambdaRestApiCloudWatchRoleB51EDA01", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "apigatewaysqsLambdaRestApiCloudWatchRoleB51EDA01": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "apigatewaysqsRestApi03BFD711": Object { + "Properties": Object { + "EndpointConfiguration": Object { + "Types": Array [ + "EDGE", + ], + }, + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "apigatewaysqsRestApiDeployment823C310Bb49e111d67a99651ecb38dc4ffc69e8b": Object { + "DependsOn": Array [ + "apigatewaysqsRestApiGET13C64342", + "apigatewaysqsRestApimessageDELETE46195B92", + "apigatewaysqsRestApimessageC2D606D3", + "apigatewaysqsRestApiPOST3638C367", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "apigatewaysqsRestApiDeploymentStageprodAA3C7DD5": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsApiAccessLogGroup4D14D1D7", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "apigatewaysqsRestApiDeployment823C310Bb49e111d67a99651ecb38dc4ffc69e8b", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "apigatewaysqsRestApiGET13C64342": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "GET", + "Integration": Object { + "Credentials": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsapigatewayrole2BA120D3", + "Arn", + ], + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": Array [ + Object { + "StatusCode": "200", + }, + Object { + "ResponseTemplates": Object { + "text/html": "Error", + }, + "SelectionPattern": "500", + "StatusCode": "500", + }, + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": Object { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'", + }, + "RequestTemplates": Object { + "application/json": "Action=ReceiveMessage", + }, + "Type": "AWS", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":sqs:path/", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "QueueName", + ], + }, + ], + ], + }, + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "200", + }, + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "500", + }, + ], + "ResourceId": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsRestApi03BFD711", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "apigatewaysqsRestApiPOST3638C367": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "POST", + "Integration": Object { + "Credentials": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsapigatewayrole2BA120D3", + "Arn", + ], + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": Array [ + Object { + "StatusCode": "200", + }, + Object { + "ResponseTemplates": Object { + "text/html": "Error", + }, + "SelectionPattern": "500", + "StatusCode": "500", + }, + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": Object { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'", + }, + "RequestTemplates": Object { + "application/json": "Action=SendMessage&MessageBody=$util.urlEncode(\\"$input.body\\")", + }, + "Type": "AWS", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":sqs:path/", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "QueueName", + ], + }, + ], + ], + }, + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "200", + }, + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "500", + }, + ], + "ResourceId": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsRestApi03BFD711", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "apigatewaysqsRestApiUsagePlan744FD0EB": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + "Stage": Object { + "Ref": "apigatewaysqsRestApiDeploymentStageprodAA3C7DD5", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "apigatewaysqsRestApimessageC2D606D3": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsRestApi03BFD711", + "RootResourceId", + ], + }, + "PathPart": "message", + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "apigatewaysqsRestApimessageDELETE46195B92": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "DELETE", + "Integration": Object { + "Credentials": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsapigatewayrole2BA120D3", + "Arn", + ], + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": Array [ + Object { + "StatusCode": "200", + }, + Object { + "ResponseTemplates": Object { + "text/html": "Error", + }, + "SelectionPattern": "500", + "StatusCode": "500", + }, + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": Object { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'", + }, + "RequestTemplates": Object { + "application/json": "Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))", + }, + "Type": "AWS", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":sqs:path/", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "QueueName", + ], + }, + ], + ], + }, + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "200", + }, + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "500", + }, + ], + "ResourceId": Object { + "Ref": "apigatewaysqsRestApimessageC2D606D3", + }, + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "apigatewaysqsapigatewayrole2BA120D3": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "apigatewaysqsapigatewayroleDefaultPolicyD83F1724": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + Object { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "Arn", + ], + }, + }, + Object { + "Action": "sqs:ReceiveMessage", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "Arn", + ], + }, + }, + Object { + "Action": "sqs:DeleteMessage", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "apigatewaysqsapigatewayroleDefaultPolicyD83F1724", + "Roles": Array [ + Object { + "Ref": "apigatewaysqsapigatewayrole2BA120D3", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "deadLetterQueue3F848E28": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + "Type": "AWS::SQS::Queue", + }, + "queue276F7297": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + "RedrivePolicy": Object { + "deadLetterTargetArn": Object { + "Fn::GetAtt": Array [ + "deadLetterQueue3F848E28", + "Arn", + ], + }, + "maxReceiveCount": 3, + }, + }, + "Type": "AWS::SQS::Queue", + }, + }, +} +`; + +exports[`Test deployment w/o DLQ 1`] = ` +Object { + "Outputs": Object { + "apigatewaysqsRestApiEndpointD55C9F0A": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "apigatewaysqsRestApiDeploymentStageprodAA3C7DD5", + }, + "/", + ], + ], + }, + }, + }, + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsapigatewayrole2BA120D3", + "Arn", + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "apigatewaysqsApiAccessLogGroup4D14D1D7": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "apigatewaysqsLambdaRestApiAccount8FA59342": Object { + "DependsOn": Array [ + "apigatewaysqsRestApi03BFD711", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsLambdaRestApiCloudWatchRoleB51EDA01", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "apigatewaysqsLambdaRestApiCloudWatchRoleB51EDA01": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "apigatewaysqsRestApi03BFD711": Object { + "Properties": Object { + "EndpointConfiguration": Object { + "Types": Array [ + "EDGE", + ], + }, + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "apigatewaysqsRestApiDeployment823C310Bb49e111d67a99651ecb38dc4ffc69e8b": Object { + "DependsOn": Array [ + "apigatewaysqsRestApiGET13C64342", + "apigatewaysqsRestApimessageDELETE46195B92", + "apigatewaysqsRestApimessageC2D606D3", + "apigatewaysqsRestApiPOST3638C367", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "apigatewaysqsRestApiDeploymentStageprodAA3C7DD5": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsApiAccessLogGroup4D14D1D7", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "apigatewaysqsRestApiDeployment823C310Bb49e111d67a99651ecb38dc4ffc69e8b", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "apigatewaysqsRestApiGET13C64342": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "GET", + "Integration": Object { + "Credentials": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsapigatewayrole2BA120D3", + "Arn", + ], + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": Array [ + Object { + "StatusCode": "200", + }, + Object { + "ResponseTemplates": Object { + "text/html": "Error", + }, + "SelectionPattern": "500", + "StatusCode": "500", + }, + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": Object { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'", + }, + "RequestTemplates": Object { + "application/json": "Action=ReceiveMessage", + }, + "Type": "AWS", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":sqs:path/", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "QueueName", + ], + }, + ], + ], + }, + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "200", + }, + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "500", + }, + ], + "ResourceId": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsRestApi03BFD711", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "apigatewaysqsRestApiPOST3638C367": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "POST", + "Integration": Object { + "Credentials": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsapigatewayrole2BA120D3", + "Arn", + ], + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": Array [ + Object { + "StatusCode": "200", + }, + Object { + "ResponseTemplates": Object { + "text/html": "Error", + }, + "SelectionPattern": "500", + "StatusCode": "500", + }, + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": Object { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'", + }, + "RequestTemplates": Object { + "application/json": "Action=SendMessage&MessageBody=$util.urlEncode(\\"$input.body\\")", + }, + "Type": "AWS", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":sqs:path/", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "QueueName", + ], + }, + ], + ], + }, + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "200", + }, + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "500", + }, + ], + "ResourceId": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsRestApi03BFD711", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "apigatewaysqsRestApiUsagePlan744FD0EB": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + "Stage": Object { + "Ref": "apigatewaysqsRestApiDeploymentStageprodAA3C7DD5", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "apigatewaysqsRestApimessageC2D606D3": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsRestApi03BFD711", + "RootResourceId", + ], + }, + "PathPart": "message", + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "apigatewaysqsRestApimessageDELETE46195B92": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "DELETE", + "Integration": Object { + "Credentials": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsapigatewayrole2BA120D3", + "Arn", + ], + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": Array [ + Object { + "StatusCode": "200", + }, + Object { + "ResponseTemplates": Object { + "text/html": "Error", + }, + "SelectionPattern": "500", + "StatusCode": "500", + }, + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": Object { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'", + }, + "RequestTemplates": Object { + "application/json": "Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))", + }, + "Type": "AWS", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":sqs:path/", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "QueueName", + ], + }, + ], + ], + }, + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "200", + }, + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "500", + }, + ], + "ResourceId": Object { + "Ref": "apigatewaysqsRestApimessageC2D606D3", + }, + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "apigatewaysqsapigatewayrole2BA120D3": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "apigatewaysqsapigatewayroleDefaultPolicyD83F1724": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + Object { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "Arn", + ], + }, + }, + Object { + "Action": "sqs:ReceiveMessage", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "Arn", + ], + }, + }, + Object { + "Action": "sqs:DeleteMessage", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "apigatewaysqsapigatewayroleDefaultPolicyD83F1724", + "Roles": Array [ + Object { + "Ref": "apigatewaysqsapigatewayrole2BA120D3", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "deadLetterQueue3F848E28": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + "Type": "AWS::SQS::Queue", + }, + "queue276F7297": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + "RedrivePolicy": Object { + "deadLetterTargetArn": Object { + "Fn::GetAtt": Array [ + "deadLetterQueue3F848E28", + "Arn", + ], + }, + "maxReceiveCount": 3, + }, + }, + "Type": "AWS::SQS::Queue", + }, + }, +} +`; + +exports[`Test minimal deployment 1`] = ` +Object { + "Outputs": Object { + "apigatewaysqsRestApiEndpointD55C9F0A": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "apigatewaysqsRestApiDeploymentStageprodAA3C7DD5", + }, + "/", + ], + ], + }, + }, + }, + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + Object { + "Action": Array [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsapigatewayrole2BA120D3", + "Arn", + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "apigatewaysqsApiAccessLogGroup4D14D1D7": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "apigatewaysqsLambdaRestApiAccount8FA59342": Object { + "DependsOn": Array [ + "apigatewaysqsRestApi03BFD711", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsLambdaRestApiCloudWatchRoleB51EDA01", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "apigatewaysqsLambdaRestApiCloudWatchRoleB51EDA01": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "apigatewaysqsRestApi03BFD711": Object { + "Properties": Object { + "EndpointConfiguration": Object { + "Types": Array [ + "EDGE", + ], + }, + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "apigatewaysqsRestApiDeployment823C310B94660621187ecc4f60bbf8643bc7537d": Object { + "DependsOn": Array [ + "apigatewaysqsRestApiGET13C64342", + "apigatewaysqsRestApimessageC2D606D3", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "apigatewaysqsRestApiDeploymentStageprodAA3C7DD5": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsApiAccessLogGroup4D14D1D7", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "apigatewaysqsRestApiDeployment823C310B94660621187ecc4f60bbf8643bc7537d", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "apigatewaysqsRestApiGET13C64342": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "GET", + "Integration": Object { + "Credentials": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsapigatewayrole2BA120D3", + "Arn", + ], + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": Array [ + Object { + "StatusCode": "200", + }, + Object { + "ResponseTemplates": Object { + "text/html": "Error", + }, + "SelectionPattern": "500", + "StatusCode": "500", + }, + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": Object { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'", + }, + "RequestTemplates": Object { + "application/json": "Action=ReceiveMessage", + }, + "Type": "AWS", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":sqs:path/", + Object { + "Ref": "AWS::AccountId", + }, + "/", + Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "QueueName", + ], + }, + ], + ], + }, + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "200", + }, + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "500", + }, + ], + "ResourceId": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsRestApi03BFD711", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "apigatewaysqsRestApiUsagePlan744FD0EB": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + "Stage": Object { + "Ref": "apigatewaysqsRestApiDeploymentStageprodAA3C7DD5", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "apigatewaysqsRestApimessageC2D606D3": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "apigatewaysqsRestApi03BFD711", + "RootResourceId", + ], + }, + "PathPart": "message", + "RestApiId": Object { + "Ref": "apigatewaysqsRestApi03BFD711", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "apigatewaysqsapigatewayrole2BA120D3": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "apigatewaysqsapigatewayroleDefaultPolicyD83F1724": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + Object { + "Action": "sqs:ReceiveMessage", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "apigatewaysqsapigatewayroleDefaultPolicyD83F1724", + "Roles": Array [ + Object { + "Ref": "apigatewaysqsapigatewayrole2BA120D3", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "deadLetterQueue3F848E28": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + "Type": "AWS::SQS::Queue", + }, + "queue276F7297": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + "RedrivePolicy": Object { + "deadLetterTargetArn": Object { + "Fn::GetAtt": Array [ + "deadLetterQueue3F848E28", + "Arn", + ], + }, + "maxReceiveCount": 3, + }, + }, + "Type": "AWS::SQS::Queue", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/apigateway-sqs.test.ts b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/apigateway-sqs.test.ts new file mode 100644 index 000000000..2fdbf63a3 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/apigateway-sqs.test.ts @@ -0,0 +1,108 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { Stack } from "@aws-cdk/core"; +import { ApiGatewayToSqs } from '../lib'; +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +// -------------------------------------------------------------- +// Test minimal deployment +// -------------------------------------------------------------- +test('Test minimal deployment', () => { + // Stack + const stack = new Stack(); + // Helper declaration + new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test deployment w/ DLQ +// -------------------------------------------------------------- +test('Test deployment w/ DLQ', () => { + // Stack + const stack = new Stack(); + // Helper declaration + new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + apiGatewayProps: {}, + queueProps: {}, + encryptionKeyProps: {}, + createRequestTemplate: "{}", + allowCreateOperation: true, + allowReadOperation: true, + allowDeleteOperation: true, + deployDeadLetterQueue: true + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test deployment w/o DLQ +// -------------------------------------------------------------- +test('Test deployment w/o DLQ', () => { + // Stack + const stack = new Stack(); + // Helper declaration + new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + apiGatewayProps: {}, + queueProps: {}, + encryptionKeyProps: {}, + createRequestTemplate: "{}", + allowCreateOperation: true, + allowReadOperation: false, + allowDeleteOperation: true, + deployDeadLetterQueue: false + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // Assertion 2 + expect(stack).toHaveResourceLike("AWS::ApiGateway::Method", { + HttpMethod: "GET", + AuthorizationType: "AWS_IAM" + }); + // Assertion 3 + expect(stack).toHaveResourceLike("AWS::ApiGateway::Method", { + HttpMethod: "POST", + AuthorizationType: "AWS_IAM" + }); + // Assertion 4 + expect(stack).toHaveResourceLike("AWS::ApiGateway::Method", { + HttpMethod: "DELETE", + AuthorizationType: "AWS_IAM" + }); +}); + +// -------------------------------------------------------------- +// Test the getter methods +// -------------------------------------------------------------- +test('Test the getter methods', () => { + // Stack + const stack = new Stack(); + // Helper declaration + const pattern = new ApiGatewayToSqs(stack, 'api-gateway-sqs', { + apiGatewayProps: {}, + queueProps: {}, + encryptionKeyProps: {}, + deployDeadLetterQueue: true, + maxReceiveCount: 3 + }); + // Assertion 1 + expect(pattern.api()).toBeDefined(); + // Assertion 2 + expect(pattern.sqsQueue()).toBeDefined(); +}); diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/integ.apigateway-sqs-crud.expected.json b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/integ.apigateway-sqs-crud.expected.json new file mode 100644 index 000000000..da2041d5e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/integ.apigateway-sqs-crud.expected.json @@ -0,0 +1,634 @@ +{ + "Description": "Integration Test for aws-apigateway-sqs", + "Resources": { + "testapigatewaysqsRestApi557C7EDC": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "testapigatewaysqsRestApiDeploymentCA19D372c3b49be7d97efd65e90b7b5084cd80d8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaysqsRestApi557C7EDC" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "testapigatewaysqsRestApiGET4AA265C9", + "testapigatewaysqsRestApimessageDELETE2D4539B7", + "testapigatewaysqsRestApimessage6D62B7B0", + "testapigatewaysqsRestApiPOST26D15DBA" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "testapigatewaysqsRestApiDeploymentStageprod1C007159": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaysqsRestApi557C7EDC" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testapigatewaysqsApiAccessLogGroup37AB0350", + "Arn" + ] + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + }, + "DeploymentId": { + "Ref": "testapigatewaysqsRestApiDeploymentCA19D372c3b49be7d97efd65e90b7b5084cd80d8" + }, + "MethodSettings": [ + { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod" + } + }, + "testapigatewaysqsRestApimessage6D62B7B0": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testapigatewaysqsRestApi557C7EDC", + "RootResourceId" + ] + }, + "PathPart": "message", + "RestApiId": { + "Ref": "testapigatewaysqsRestApi557C7EDC" + } + } + }, + "testapigatewaysqsRestApimessageDELETE2D4539B7": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "DELETE", + "ResourceId": { + "Ref": "testapigatewaysqsRestApimessage6D62B7B0" + }, + "RestApiId": { + "Ref": "testapigatewaysqsRestApi557C7EDC" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaysqsapigatewayrole07110CD6", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" + }, + "RequestTemplates": { + "application/json": "Action=DeleteMessage&ReceiptHandle=$util.urlEncode($input.params('receiptHandle'))" + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":sqs:path/", + { + "Ref": "AWS::AccountId" + }, + "/", + { + "Fn::GetAtt": [ + "queue276F7297", + "QueueName" + ] + } + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaysqsRestApiPOST26D15DBA": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "ResourceId": { + "Fn::GetAtt": [ + "testapigatewaysqsRestApi557C7EDC", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "testapigatewaysqsRestApi557C7EDC" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaysqsapigatewayrole07110CD6", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" + }, + "RequestTemplates": { + "application/json": "Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")" + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":sqs:path/", + { + "Ref": "AWS::AccountId" + }, + "/", + { + "Fn::GetAtt": [ + "queue276F7297", + "QueueName" + ] + } + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaysqsRestApiGET4AA265C9": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Fn::GetAtt": [ + "testapigatewaysqsRestApi557C7EDC", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "testapigatewaysqsRestApi557C7EDC" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaysqsapigatewayrole07110CD6", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" + }, + "RequestTemplates": { + "application/json": "Action=ReceiveMessage" + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":sqs:path/", + { + "Ref": "AWS::AccountId" + }, + "/", + { + "Fn::GetAtt": [ + "queue276F7297", + "QueueName" + ] + } + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaysqsRestApiUsagePlan2295EB95": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "testapigatewaysqsRestApi557C7EDC" + }, + "Stage": { + "Ref": "testapigatewaysqsRestApiDeploymentStageprod1C007159" + }, + "Throttle": {} + } + ] + } + }, + "testapigatewaysqsApiAccessLogGroup37AB0350": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testapigatewaysqsLambdaRestApiCloudWatchRoleF10D0F78": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "testapigatewaysqsLambdaRestApiAccountACC6BE82": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "testapigatewaysqsLambdaRestApiCloudWatchRoleF10D0F78", + "Arn" + ] + } + }, + "DependsOn": [ + "testapigatewaysqsRestApi557C7EDC" + ] + }, + "testapigatewaysqsapigatewayrole07110CD6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testapigatewaysqsapigatewayroleDefaultPolicy052E7AD5": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + } + }, + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "queue276F7297", + "Arn" + ] + } + }, + { + "Action": "sqs:ReceiveMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "queue276F7297", + "Arn" + ] + } + }, + { + "Action": "sqs:DeleteMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "queue276F7297", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testapigatewaysqsapigatewayroleDefaultPolicy052E7AD5", + "Roles": [ + { + "Ref": "testapigatewaysqsapigatewayrole07110CD6" + } + ] + } + }, + "EncryptionKey1B843E66": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "testapigatewaysqsapigatewayrole07110CD6", + "Arn" + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "EnableKeyRotation": true + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "deadLetterQueue3F848E28": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + } + } + }, + "queue276F7297": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + }, + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "deadLetterQueue3F848E28", + "Arn" + ] + }, + "maxReceiveCount": 3 + } + } + } + }, + "Outputs": { + "testapigatewaysqsRestApiEndpointD98015FF": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testapigatewaysqsRestApi557C7EDC" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testapigatewaysqsRestApiDeploymentStageprod1C007159" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/integ.apigateway-sqs-crud.ts b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/integ.apigateway-sqs-crud.ts new file mode 100644 index 000000000..e913a8800 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/integ.apigateway-sqs-crud.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { ApiGatewayToSqs, ApiGatewayToSqsProps } from "../lib"; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-apigateway-sqs'); +stack.templateOptions.description = 'Integration Test for aws-apigateway-sqs'; + +// Definitions +const props: ApiGatewayToSqsProps = { + apiGatewayProps: {}, + queueProps: {}, + encryptionKeyProps: {}, + allowReadOperation: true, + allowCreateOperation: true, + allowDeleteOperation: true, + createRequestTemplate: "{\r\n \"QueueUrl\": \"${QueueUrl}\",\r\n \"MessageBody\": \"${MessageBody}\"\r\n}" +}; + +new ApiGatewayToSqs(stack, 'test-api-gateway-sqs', props); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..297d2a80e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/integ.no-arguments.expected.json @@ -0,0 +1,445 @@ +{ + "Description": "Integration Test for aws-apigateway-sqs", + "Resources": { + "testapigatewaysqsdefaultRestApi554243C3": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "testapigatewaysqsdefaultRestApiDeploymentFB9688F515062e1c331d835b42c02aa42f85db7f": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaysqsdefaultRestApi554243C3" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "testapigatewaysqsdefaultRestApiGET733E6394", + "testapigatewaysqsdefaultRestApimessage41073D7F" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "testapigatewaysqsdefaultRestApiDeploymentStageprod600FEEE2": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "testapigatewaysqsdefaultRestApi554243C3" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultApiAccessLogGroup16132600", + "Arn" + ] + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + }, + "DeploymentId": { + "Ref": "testapigatewaysqsdefaultRestApiDeploymentFB9688F515062e1c331d835b42c02aa42f85db7f" + }, + "MethodSettings": [ + { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod" + } + }, + "testapigatewaysqsdefaultRestApimessage41073D7F": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultRestApi554243C3", + "RootResourceId" + ] + }, + "PathPart": "message", + "RestApiId": { + "Ref": "testapigatewaysqsdefaultRestApi554243C3" + } + } + }, + "testapigatewaysqsdefaultRestApiGET733E6394": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "ResourceId": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultRestApi554243C3", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "testapigatewaysqsdefaultRestApi554243C3" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "Credentials": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultapigatewayrole080B85EC", + "Arn" + ] + }, + "IntegrationHttpMethod": "POST", + "IntegrationResponses": [ + { + "StatusCode": "200" + }, + { + "ResponseTemplates": { + "text/html": "Error" + }, + "SelectionPattern": "500", + "StatusCode": "500" + } + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" + }, + "RequestTemplates": { + "application/json": "Action=ReceiveMessage" + }, + "Type": "AWS", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":sqs:path/", + { + "Ref": "AWS::AccountId" + }, + "/", + { + "Fn::GetAtt": [ + "queue276F7297", + "QueueName" + ] + } + ] + ] + } + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "200" + }, + { + "ResponseParameters": { + "method.response.header.Content-Type": true + }, + "StatusCode": "500" + } + ] + } + }, + "testapigatewaysqsdefaultRestApiUsagePlan3475CA67": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "testapigatewaysqsdefaultRestApi554243C3" + }, + "Stage": { + "Ref": "testapigatewaysqsdefaultRestApiDeploymentStageprod600FEEE2" + }, + "Throttle": {} + } + ] + } + }, + "testapigatewaysqsdefaultApiAccessLogGroup16132600": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testapigatewaysqsdefaultLambdaRestApiCloudWatchRole8EA3C5EC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "testapigatewaysqsdefaultLambdaRestApiAccountF7D19F4F": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultLambdaRestApiCloudWatchRole8EA3C5EC", + "Arn" + ] + } + }, + "DependsOn": [ + "testapigatewaysqsdefaultRestApi554243C3" + ] + }, + "testapigatewaysqsdefaultapigatewayrole080B85EC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testapigatewaysqsdefaultapigatewayroleDefaultPolicyFF253592": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + } + }, + { + "Action": "sqs:ReceiveMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "queue276F7297", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testapigatewaysqsdefaultapigatewayroleDefaultPolicyFF253592", + "Roles": [ + { + "Ref": "testapigatewaysqsdefaultapigatewayrole080B85EC" + } + ] + } + }, + "EncryptionKey1B843E66": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "testapigatewaysqsdefaultapigatewayrole080B85EC", + "Arn" + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "EnableKeyRotation": true + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "deadLetterQueue3F848E28": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + } + } + }, + "queue276F7297": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + }, + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "deadLetterQueue3F848E28", + "Arn" + ] + }, + "maxReceiveCount": 3 + } + } + } + }, + "Outputs": { + "testapigatewaysqsdefaultRestApiEndpointE6DCCE4E": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testapigatewaysqsdefaultRestApi554243C3" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testapigatewaysqsdefaultRestApiDeploymentStageprod600FEEE2" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/integ.no-arguments.ts new file mode 100644 index 000000000..3da9f042d --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-apigateway-sqs/test/integ.no-arguments.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { ApiGatewayToSqs, ApiGatewayToSqsProps } from "../lib"; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-apigateway-sqs-default'); +stack.templateOptions.description = 'Integration Test for aws-apigateway-sqs'; + +// Definitions +const props: ApiGatewayToSqsProps = { + apiGatewayProps: {}, + queueProps: {}, + encryptionKeyProps: {} +}; + +new ApiGatewayToSqs(stack, 'test-api-gateway-sqs-default', props); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/README.md b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/README.md new file mode 100644 index 000000000..2f410cda8 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/README.md @@ -0,0 +1,82 @@ +# aws-cloudfront-apigateway-lambda module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-cloudfront-apigateway-lambda/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_cloudfront_apigateway_lambda`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda`| + +This AWS Solutions Konstruk implements an AWS Cloudfront fronting an Amazon API Gateway Lambda backed REST API. + +Here is a minimal deployable pattern definition: + +``` javascript +import * as defaults from '@aws-solutions-konstruk/core'; +import { CloudFrontToApiGatewayToLambda } from '@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda'; + +const stack = new Stack(); + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' +}; + +new CloudFrontToApiGatewayToLambda(stack, 'test-cloudfront-apigateway-lambda', { + lambdaFunctionProps: lambdaProps, + deployLambda: true +}); +``` + +## Initializer + +``` text +new CloudFrontToApiGatewayToLambda(scope: Construct, id: string, props: CloudFrontToApiGatewayToLambdaProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`CloudFrontToApiGatewayToLambdaProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Existing instance of Lambda Function object| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user provided props to override the default props for Lambda function| +|apiGatewayProps?|[`api.LambdaRestApiProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.LambdaRestApiProps.html)|Optional user provided props to override the default props for API Gateway| +|cloudFrontDistributionProps?|[`cloudfront.CloudFrontWebDistributionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudfront.CloudFrontWebDistributionProps.html)|Optional user provided props to override the default props for Cloudfront Distribution| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|cloudFrontWebDistribution()|[`cloudfront.CloudFrontWebDistribution`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudfront.CloudFrontWebDistribution.html)|Retruns an instance of cloudfront.CloudFrontWebDistribution created by the construct| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Returns an instance of the Lambda function created by the pattern.| +|restApi()|[`api.RestApi`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.RestApi.html)|Returns an instance of the API Gateway REST API created by the pattern.| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/architecture.png new file mode 100644 index 000000000..9ec45f017 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/lib/index.ts new file mode 100644 index 000000000..d2aef5da6 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/lib/index.ts @@ -0,0 +1,121 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as api from '@aws-cdk/aws-apigateway'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; +import { CloudFrontToApiGateway } from '@aws-solutions-konstruk/aws-cloudfront-apigateway'; + +/** + * @summary The properties for the CloudFrontToApiGatewayToLambda Construct + */ +export interface CloudFrontToApiGatewayToLambdaProps { + /** + * Whether to create a new Lambda function or use an existing Lambda function. + * If set to false, you must provide a lambda function object as `existingLambdaObj` + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided props to override the default props for the Lambda function. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly lambdaFunctionProps?: lambda.FunctionProps + /** + * Optional user provided props to override the default props for the API Gateway. + * + * @default - Default props are used + */ + readonly apiGatewayProps?: api.LambdaRestApiProps + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly cloudFrontDistributionProps?: cloudfront.CloudFrontWebDistributionProps +} + +export class CloudFrontToApiGatewayToLambda extends Construct { + private cloudfront: cloudfront.CloudFrontWebDistribution; + private api: api.RestApi; + private fn: lambda.Function; + + /** + * @summary Constructs a new instance of the CloudFrontToApiGatewayToLambda class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {CloudFrontToApiGatewayToLambdaProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: CloudFrontToApiGatewayToLambdaProps) { + super(scope, id); + + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + + this.api = defaults.RegionalLambdaRestApi(this, this.fn, props.apiGatewayProps); + + const apiCloudfront: CloudFrontToApiGateway = new CloudFrontToApiGateway(this, 'CloudFrontToApiGateway', { + existingApiGatewayObj: this.api, + cloudFrontDistributionProps: props.cloudFrontDistributionProps + }); + + this.cloudfront = apiCloudfront.cloudFrontWebDistribution(); + } + + /** + * @summary Retruns an instance of cloudfront.CloudFrontWebDistribution created by the construct. + * @returns {cloudfront.CloudFrontWebDistribution} Instance of CloudFrontWebDistribution created by the construct + * @since 0.8.0 + * @access public + */ + public cloudFrontWebDistribution(): cloudfront.CloudFrontWebDistribution { + return this.cloudfront; + } + + /** + * @summary Retruns an instance of api.RestApi created by the construct. + * @returns {api.RestApi} Instance of RestApi created by the construct + * @since 0.8.0 + * @access public + */ + public restApi(): api.RestApi { + return this.api; + } + + /** + * @summary Retruns an instance of lambda.Function created by the construct. + * @returns {lambda.Function} Instance of Function created by the construct + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/package.json b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/package.json new file mode 100644 index 000000000..6b23b7d0c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/package.json @@ -0,0 +1,83 @@ +{ + "name": "@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda", + "version": "0.8.0", + "description": "CDK Constructs for AWS Cloudfront to AWS API Gateway to AWS Lambda integration.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.cloudfrontapigatewaylambda", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "cloudfrontapigatewaylambda" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.CloudfrontApiGatewayLambda", + "packageId": "Amazon.Konstruk.AWS.CloudfrontApiGatewayLambda", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-cloudfront-apigateway-lambda", + "module": "aws_solutions_konstruk.aws_cloudfront_apigateway_lambda" + } + } + }, + "dependencies": { + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-cloudfront": "~1.25.0", + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-logs": "~1.25.0", + "@aws-solutions-konstruk/aws-cloudfront-apigateway": "~0.8.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-cloudfront": "~1.25.0", + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-logs": "~1.25.0", + "@aws-solutions-konstruk/aws-cloudfront-apigateway": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/__snapshots__/test.cloudfront-apigateway-lambda.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/__snapshots__/test.cloudfront-apigateway-lambda.test.js.snap new file mode 100644 index 000000000..e9a74b4b5 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/__snapshots__/test.cloudfront-apigateway-lambda.test.js.snap @@ -0,0 +1,720 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test CloudFrontToApiGatewayToLambda default params 1`] = ` +Object { + "Outputs": Object { + "testcloudfrontapigatewaylambdaRestApiEndpoint0A6CB43E": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "testcloudfrontapigatewaylambdaRestApiDeploymentStageprod9373DCA0", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": Object { + "Description": "Artifact hash for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": Object { + "Description": "S3 bucket for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": Object { + "Description": "S3 key for asset version \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "testcloudfrontapigatewaylambdaApiAccessLogGroup97EB2E40": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "testcloudfrontapigatewaylambdaCloudFrontToApiGatewayCloudFrontDistributionCFDistribution4AF2BFE4": Object { + "Properties": Object { + "DistributionConfig": Object { + "DefaultCacheBehavior": Object { + "AllowedMethods": Array [ + "GET", + "HEAD", + ], + "CachedMethods": Array [ + "GET", + "HEAD", + ], + "Compress": true, + "ForwardedValues": Object { + "Cookies": Object { + "Forward": "none", + }, + "QueryString": false, + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https", + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": Object { + "Bucket": Object { + "Fn::GetAtt": Array [ + "testcloudfrontapigatewaylambdaCloudFrontToApiGatewayCloudfrontLoggingBucket7F467421", + "RegionalDomainName", + ], + }, + "IncludeCookies": false, + }, + "Origins": Array [ + Object { + "CustomOriginConfig": Object { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginKeepaliveTimeout": 5, + "OriginProtocolPolicy": "https-only", + "OriginReadTimeout": 30, + "OriginSSLProtocols": Array [ + "TLSv1.2", + ], + }, + "DomainName": Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "/", + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "://", + Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "testcloudfrontapigatewaylambdaRestApiDeploymentStageprod9373DCA0", + }, + "/", + ], + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + "Id": "origin1", + }, + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": Object { + "CloudFrontDefaultCertificate": true, + }, + }, + }, + "Type": "AWS::CloudFront::Distribution", + }, + "testcloudfrontapigatewaylambdaCloudFrontToApiGatewayCloudfrontLoggingBucket7F467421": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + Object { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "testcloudfrontapigatewaylambdaLambdaRestApiAccount1A4578BB": Object { + "DependsOn": Array [ + "testcloudfrontapigatewaylambdaRestApi96B9C695", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "testcloudfrontapigatewaylambdaLambdaRestApiCloudWatchRole7A327F48", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "testcloudfrontapigatewaylambdaLambdaRestApiCloudWatchRole7A327F48": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "testcloudfrontapigatewaylambdaRestApi96B9C695": Object { + "Properties": Object { + "EndpointConfiguration": Object { + "Types": Array [ + "REGIONAL", + ], + }, + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "testcloudfrontapigatewaylambdaRestApiANY293F8177": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Fn::GetAtt": Array [ + "testcloudfrontapigatewaylambdaRestApi96B9C695", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "testcloudfrontapigatewaylambdaRestApiANYApiPermissionTesttestcloudfrontapigatewaylambdaRestApiB469ACF4ANY1049443E": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695", + }, + "/test-invoke-stage/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "testcloudfrontapigatewaylambdaRestApiANYApiPermissiontestcloudfrontapigatewaylambdaRestApiB469ACF4ANY60888502": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695", + }, + "/", + Object { + "Ref": "testcloudfrontapigatewaylambdaRestApiDeploymentStageprod9373DCA0", + }, + "/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "testcloudfrontapigatewaylambdaRestApiDeployment3AAD18356ddf1e04746630aef78bc65a8edbab85": Object { + "DependsOn": Array [ + "testcloudfrontapigatewaylambdaRestApiproxyANY21F2417C", + "testcloudfrontapigatewaylambdaRestApiproxyBFEB2E30", + "testcloudfrontapigatewaylambdaRestApiANY293F8177", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "testcloudfrontapigatewaylambdaRestApiDeploymentStageprod9373DCA0": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "testcloudfrontapigatewaylambdaApiAccessLogGroup97EB2E40", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "testcloudfrontapigatewaylambdaRestApiDeployment3AAD18356ddf1e04746630aef78bc65a8edbab85", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "testcloudfrontapigatewaylambdaRestApiUsagePlan283916E7": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695", + }, + "Stage": Object { + "Ref": "testcloudfrontapigatewaylambdaRestApiDeploymentStageprod9373DCA0", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "testcloudfrontapigatewaylambdaRestApiproxyANY21F2417C": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "testcloudfrontapigatewaylambdaRestApiproxyBFEB2E30", + }, + "RestApiId": Object { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "testcloudfrontapigatewaylambdaRestApiproxyANYApiPermissionTesttestcloudfrontapigatewaylambdaRestApiB469ACF4ANYproxy3BCE2E38": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695", + }, + "/test-invoke-stage/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "testcloudfrontapigatewaylambdaRestApiproxyANYApiPermissiontestcloudfrontapigatewaylambdaRestApiB469ACF4ANYproxyA51D7C81": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695", + }, + "/", + Object { + "Ref": "testcloudfrontapigatewaylambdaRestApiDeploymentStageprod9373DCA0", + }, + "/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "testcloudfrontapigatewaylambdaRestApiproxyBFEB2E30": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "testcloudfrontapigatewaylambdaRestApi96B9C695", + "RootResourceId", + ], + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..e6f8073ec --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/integ.no-arguments.expected.json @@ -0,0 +1,717 @@ +{ + "Description": "Integration Test for aws-cloudfront-apigateway-lambda", + "Resources": { + "testcloudfrontapigatewaylambdaRestApi96B9C695": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Name": "RestApi" + } + }, + "testcloudfrontapigatewaylambdaRestApiDeployment3AAD18356ddf1e04746630aef78bc65a8edbab85": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "testcloudfrontapigatewaylambdaRestApiproxyANY21F2417C", + "testcloudfrontapigatewaylambdaRestApiproxyBFEB2E30", + "testcloudfrontapigatewaylambdaRestApiANY293F8177" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "testcloudfrontapigatewaylambdaRestApiDeploymentStageprod9373DCA0": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testcloudfrontapigatewaylambdaApiAccessLogGroup97EB2E40", + "Arn" + ] + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + }, + "DeploymentId": { + "Ref": "testcloudfrontapigatewaylambdaRestApiDeployment3AAD18356ddf1e04746630aef78bc65a8edbab85" + }, + "MethodSettings": [ + { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod" + } + }, + "testcloudfrontapigatewaylambdaRestApiproxyBFEB2E30": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testcloudfrontapigatewaylambdaRestApi96B9C695", + "RootResourceId" + ] + }, + "PathPart": "{proxy+}", + "RestApiId": { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695" + } + } + }, + "testcloudfrontapigatewaylambdaRestApiproxyANYApiPermissiontestcloudfrontapigatewaylambdastacktestcloudfrontapigatewaylambdaRestApiEFF71B39ANYproxy384A83AC": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695" + }, + "/", + { + "Ref": "testcloudfrontapigatewaylambdaRestApiDeploymentStageprod9373DCA0" + }, + "/*/{proxy+}" + ] + ] + } + } + }, + "testcloudfrontapigatewaylambdaRestApiproxyANYApiPermissionTesttestcloudfrontapigatewaylambdastacktestcloudfrontapigatewaylambdaRestApiEFF71B39ANYproxy616372B7": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695" + }, + "/test-invoke-stage/*/{proxy+}" + ] + ] + } + } + }, + "testcloudfrontapigatewaylambdaRestApiproxyANY21F2417C": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Ref": "testcloudfrontapigatewaylambdaRestApiproxyBFEB2E30" + }, + "RestApiId": { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "testcloudfrontapigatewaylambdaRestApiANYApiPermissiontestcloudfrontapigatewaylambdastacktestcloudfrontapigatewaylambdaRestApiEFF71B39ANY25A0D653": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695" + }, + "/", + { + "Ref": "testcloudfrontapigatewaylambdaRestApiDeploymentStageprod9373DCA0" + }, + "/*/" + ] + ] + } + } + }, + "testcloudfrontapigatewaylambdaRestApiANYApiPermissionTesttestcloudfrontapigatewaylambdastacktestcloudfrontapigatewaylambdaRestApiEFF71B39ANYAEAD59C3": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695" + }, + "/test-invoke-stage/*/" + ] + ] + } + } + }, + "testcloudfrontapigatewaylambdaRestApiANY293F8177": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "testcloudfrontapigatewaylambdaRestApi96B9C695", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "testcloudfrontapigatewaylambdaRestApiUsagePlan283916E7": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695" + }, + "Stage": { + "Ref": "testcloudfrontapigatewaylambdaRestApiDeploymentStageprod9373DCA0" + }, + "Throttle": {} + } + ] + } + }, + "testcloudfrontapigatewaylambdaApiAccessLogGroup97EB2E40": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testcloudfrontapigatewaylambdaLambdaRestApiCloudWatchRole7A327F48": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "testcloudfrontapigatewaylambdaLambdaRestApiAccount1A4578BB": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "testcloudfrontapigatewaylambdaLambdaRestApiCloudWatchRole7A327F48", + "Arn" + ] + } + }, + "DependsOn": [ + "testcloudfrontapigatewaylambdaRestApi96B9C695" + ] + }, + "testcloudfrontapigatewaylambdaCloudFrontToApiGatewayCloudfrontLoggingBucket7F467421": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + }, + { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + } + ] + } + } + }, + "testcloudfrontapigatewaylambdaCloudFrontToApiGatewayCloudFrontDistributionCFDistribution4AF2BFE4": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "AllowedMethods": [ + "GET", + "HEAD" + ], + "CachedMethods": [ + "GET", + "HEAD" + ], + "Compress": true, + "ForwardedValues": { + "Cookies": { + "Forward": "none" + }, + "QueryString": false + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https" + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": { + "Bucket": { + "Fn::GetAtt": [ + "testcloudfrontapigatewaylambdaCloudFrontToApiGatewayCloudfrontLoggingBucket7F467421", + "RegionalDomainName" + ] + }, + "IncludeCookies": false + }, + "Origins": [ + { + "CustomOriginConfig": { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginKeepaliveTimeout": 5, + "OriginProtocolPolicy": "https-only", + "OriginReadTimeout": 30, + "OriginSSLProtocols": [ + "TLSv1.2" + ] + }, + "DomainName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "://", + { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testcloudfrontapigatewaylambdaRestApiDeploymentStageprod9373DCA0" + }, + "/" + ] + ] + } + ] + } + ] + } + ] + } + ] + }, + "Id": "origin1" + } + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": { + "CloudFrontDefaultCertificate": true + } + } + } + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + } + }, + "Outputs": { + "testcloudfrontapigatewaylambdaRestApiEndpoint0A6CB43E": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testcloudfrontapigatewaylambdaRestApi96B9C695" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testcloudfrontapigatewaylambdaRestApiDeploymentStageprod9373DCA0" + }, + "/" + ] + ] + } + } + }, + "Parameters": { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": { + "Type": "String", + "Description": "S3 bucket for asset \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": { + "Type": "String", + "Description": "S3 key for asset version \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": { + "Type": "String", + "Description": "Artifact hash for asset \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/integ.no-arguments.ts new file mode 100644 index 000000000..26df23433 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/integ.no-arguments.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { CloudFrontToApiGatewayToLambda } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-cloudfront-apigateway-lambda-stack'); +stack.templateOptions.description = 'Integration Test for aws-cloudfront-apigateway-lambda'; + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' +}; + +new CloudFrontToApiGatewayToLambda(stack, 'test-cloudfront-apigateway-lambda', { + lambdaFunctionProps: lambdaProps, + deployLambda: true +}); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/lambda/index.js new file mode 100644 index 000000000..4b3640c1e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/lambda/index.js @@ -0,0 +1,10 @@ +console.log('Loading function'); + +exports.handler = async (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); +    return { +      statusCode: 200, +      headers: { 'Content-Type': 'text/plain' }, +      body: `Hello from Project Vesper! You've hit ${event.path}\n` +    }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/test.cloudfront-apigateway-lambda.test.ts b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/test.cloudfront-apigateway-lambda.test.ts new file mode 100644 index 000000000..d817775e5 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda/test/test.cloudfront-apigateway-lambda.test.ts @@ -0,0 +1,179 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { CloudFrontToApiGatewayToLambda, CloudFrontToApiGatewayToLambdaProps } from "../lib"; +import * as cdk from "@aws-cdk/core"; +import * as lambda from '@aws-cdk/aws-lambda'; +import '@aws-cdk/assert/jest'; + +function deployNewFunc(stack: cdk.Stack) { + const lambdaFunctionProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }; + + return new CloudFrontToApiGatewayToLambda(stack, 'test-cloudfront-apigateway-lambda', { + deployLambda: true, + lambdaFunctionProps + }); +} + +function useExistingFunc(stack: cdk.Stack) { + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + return new CloudFrontToApiGatewayToLambda(stack, 'test-cloudfront-apigateway-lambda', { + deployLambda: false, + existingLambdaObj: new lambda.Function(stack, 'MyExistingFunction', lambdaFunctionProps) + }); +} + +test('snapshot test CloudFrontToApiGatewayToLambda default params', () => { + const stack = new cdk.Stack(); + deployNewFunc(stack); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: CloudFrontToApiGatewayToLambda = deployNewFunc(stack); + + expect(construct.cloudFrontWebDistribution()).toBeDefined(); + expect(construct.restApi()).toBeDefined(); + expect(construct.lambdaFunction()).toBeDefined(); +}); + +test('check lambda function properties for deploy: true', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + Runtime: "nodejs10.x", + Environment: { + Variables: { + AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1" + } + } + }); +}); + +test('check lambda function role for deploy: false', () => { + const stack = new cdk.Stack(); + + useExistingFunc(stack); + + expect(stack).toHaveResource('AWS::IAM::Role', { + 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" + ] + ] + } + ] + }); +}); + +test('check exception for Missing existingObj from props for deploy = false', () => { + const stack = new cdk.Stack(); + + const props: CloudFrontToApiGatewayToLambdaProps = { + deployLambda: false + }; + + try { + new CloudFrontToApiGatewayToLambda(stack, 'test-cloudfront-apigateway-lambda', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); + +test('check deploy = true and no prop', () => { + const stack = new cdk.Stack(); + + const props: CloudFrontToApiGatewayToLambdaProps = { + deployLambda: true + }; + + try { + new CloudFrontToApiGatewayToLambda(stack, 'test-cloudfront-apigateway-lambda', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); + +test('override api gateway properties', () => { + const stack = new cdk.Stack(); + + const lambdaFunctionProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }; + + const fn: lambda.Function = new lambda.Function(stack, 'MyExistingFunction', lambdaFunctionProps); + + new CloudFrontToApiGatewayToLambda(stack, 'test-cloudfront-apigateway-lambda', { + deployLambda: false, + existingLambdaObj: fn, + apiGatewayProps: { + handler: fn, + options: { + description: "Override description" + } + } + }); + + expect(stack).toHaveResource('AWS::ApiGateway::RestApi', + { + Description: "Override description", + EndpointConfiguration: { + Types: [ + "REGIONAL" + ] + }, + Name: "RestApi" + }); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/README.md b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/README.md new file mode 100644 index 000000000..1ad33c9c5 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/README.md @@ -0,0 +1,82 @@ +# aws-cloudfront-apigateway module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-cloudfront-apigateway/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_cloudfront_apigateway`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-cloudfront-apigateway`| + +This AWS Solutions Konstruk implements an AWS Cloudfront fronting an Amazon API Gateway REST API. + +Here is a minimal deployable pattern definition: + +``` javascript +const { defaults } = require('@aws-solutions-konstruk/core'); +const { CloudFrontToApiGateway } = require('@aws-solutions-konstruk/aws-cloudfront-apigateway'); + +const stack = new Stack(); + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' +}; + +const func = defaults.deployLambdaFunction(stack, lambdaProps); + +const _api = defaults.RegionalApiGateway(stack, func); + +new CloudFrontToApiGateway(stack, 'test-cloudfront-apigateway', { + existingApiGatewayObj: _api +}); + +``` + +## Initializer + +``` text +new CloudFrontToApiGateway(scope: Construct, id: string, props: CloudFrontToApiGatewayProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`CloudFrontToApiGatewayProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|existingApiGatewayObj|[`api.RestApi`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.RestApi.html)|The regional API Gateway that will be fronted with the CloudFront| +|cloudFrontDistributionProps?|[`cloudfront.CloudFrontWebDistributionProps | any`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudfront.CloudFrontWebDistributionProps.html)|Optional user provided props to override the default props for Cloudfront Distribution| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|cloudFrontWebDistribution()|[`cloudfront.CloudFrontWebDistribution`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudfront.CloudFrontWebDistribution.html)|Retruns an instance of cloudfront.CloudFrontWebDistribution created by the construct| +|restApi()|[`api.RestApi`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.RestApi.html)|Returns an instance of the API Gateway REST API created by the pattern.| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/architecture.png new file mode 100644 index 000000000..a5a667d5a Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/lib/index.ts new file mode 100644 index 000000000..a781d7a94 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/lib/index.ts @@ -0,0 +1,77 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as api from '@aws-cdk/aws-apigateway'; +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; + +/** + * @summary The properties for the CloudFrontToApiGateway Construct + */ +export interface CloudFrontToApiGatewayProps { + /** + * Existing instance of api.RestApi object. + * + * @default - None + */ + readonly existingApiGatewayObj: api.RestApi + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly cloudFrontDistributionProps?: cloudfront.CloudFrontWebDistributionProps | any; +} + +export class CloudFrontToApiGateway extends Construct { + private cloudfront: cloudfront.CloudFrontWebDistribution; + private api: api.RestApi; + + /** + * @summary Constructs a new instance of the CloudFrontToApiGateway class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {CloudFrontToApiGatewayProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: CloudFrontToApiGatewayProps) { + super(scope, id); + + this.api = props.existingApiGatewayObj; + + this.cloudfront = defaults.CloudFrontDistributionForApiGateway(this, props.existingApiGatewayObj, + props.cloudFrontDistributionProps); + } + + /** + * @summary Retruns an instance of cloudfront.CloudFrontWebDistribution created by the construct. + * @returns {cloudfront.CloudFrontWebDistribution} Instance of CloudFrontWebDistribution created by the construct + * @since 0.8.0 + * @access public + */ + public cloudFrontWebDistribution(): cloudfront.CloudFrontWebDistribution { + return this.cloudfront; + } + + /** + * @summary Retruns an instance of api.RestApi created by the construct. + * @returns {api.RestApi} Instance of RestApi created by the construct + * @since 0.8.0 + * @access public + */ + public restApi(): api.RestApi { + return this.api; + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/package.json b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/package.json new file mode 100644 index 000000000..c8072fafb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/package.json @@ -0,0 +1,81 @@ +{ + "name": "@aws-solutions-konstruk/aws-cloudfront-apigateway", + "version": "0.8.0", + "description": "CDK Constructs for AWS Cloudfront to AWS API Gateway integration.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.cloudfrontapigateway", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "cloudfrontapigateway" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.CloudfrontApiGateway", + "packageId": "Amazon.Konstruk.AWS.CloudfrontApiGateway", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-cloudfront-apigateway", + "module": "aws_solutions_konstruk.aws_cloudfront_apigateway" + } + } + }, + "dependencies": { + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-cloudfront": "~1.25.0", + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-logs": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-cloudfront": "~1.25.0", + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-logs": "~1.25.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/__snapshots__/test.cloudfront-apigateway.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/__snapshots__/test.cloudfront-apigateway.test.js.snap new file mode 100644 index 000000000..73112c5a2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/__snapshots__/test.cloudfront-apigateway.test.js.snap @@ -0,0 +1,720 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test CloudFrontToApiGateway default params 1`] = ` +Object { + "Outputs": Object { + "RestApiEndpoint0551178A": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "RestApi0C43BF4B", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParametersd33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818ArtifactHashAC93FA3A": Object { + "Description": "Artifact hash for asset \\"d33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818\\"", + "Type": "String", + }, + "AssetParametersd33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818S3BucketC8A24561": Object { + "Description": "S3 bucket for asset \\"d33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818\\"", + "Type": "String", + }, + "AssetParametersd33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818S3VersionKey130FEE10": Object { + "Description": "S3 key for asset version \\"d33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818\\"", + "Type": "String", + }, + }, + "Resources": Object { + "ApiAccessLogGroupCEA70788": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParametersd33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818S3BucketC8A24561", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersd33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818S3VersionKey130FEE10", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersd33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818S3VersionKey130FEE10", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaRestApiAccount": Object { + "DependsOn": Array [ + "RestApi0C43BF4B", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "LambdaRestApiCloudWatchRoleF339D4E6": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "RestApi0C43BF4B": Object { + "Properties": Object { + "EndpointConfiguration": Object { + "Types": Array [ + "REGIONAL", + ], + }, + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "RestApiANYA7C1DC94": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiANYApiPermissionRestApiANY3A99B4EE": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiANYApiPermissionTestRestApiANY79BD91F2": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8": Object { + "DependsOn": Array [ + "RestApiproxyANY1786B242", + "RestApiproxyC95856DD", + "RestApiANYA7C1DC94", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "RestApiDeploymentStageprod3855DE66": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "ApiAccessLogGroupCEA70788", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "RestApiUsagePlan6E1C537A": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "Stage": Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "RestApiproxyANY1786B242": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "RestApiproxyC95856DD", + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiproxyANYApiPermissionRestApiANYproxy9C9912F9": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyANYApiPermissionTestRestApiANYproxyCB7BC56D": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyC95856DD": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "testcloudfrontapigatewayCloudFrontDistributionCFDistribution2270C4C1": Object { + "Properties": Object { + "DistributionConfig": Object { + "DefaultCacheBehavior": Object { + "AllowedMethods": Array [ + "GET", + "HEAD", + ], + "CachedMethods": Array [ + "GET", + "HEAD", + ], + "Compress": true, + "ForwardedValues": Object { + "Cookies": Object { + "Forward": "none", + }, + "QueryString": false, + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https", + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": Object { + "Bucket": Object { + "Fn::GetAtt": Array [ + "testcloudfrontapigatewayCloudfrontLoggingBucket9811F6E8", + "RegionalDomainName", + ], + }, + "IncludeCookies": false, + }, + "Origins": Array [ + Object { + "CustomOriginConfig": Object { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginKeepaliveTimeout": 5, + "OriginProtocolPolicy": "https-only", + "OriginReadTimeout": 30, + "OriginSSLProtocols": Array [ + "TLSv1.2", + ], + }, + "DomainName": Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "/", + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "://", + Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "RestApi0C43BF4B", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/", + ], + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + "Id": "origin1", + }, + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": Object { + "CloudFrontDefaultCertificate": true, + }, + }, + }, + "Type": "AWS::CloudFront::Distribution", + }, + "testcloudfrontapigatewayCloudfrontLoggingBucket9811F6E8": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + Object { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..b6789edd2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/integ.no-arguments.expected.json @@ -0,0 +1,717 @@ +{ + "Description": "Integration Test for aws-cloudfront-apigateway", + "Resources": { + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersd33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818S3BucketC8A24561" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersd33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818S3VersionKey130FEE10" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersd33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818S3VersionKey130FEE10" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "RestApi0C43BF4B": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Name": "RestApi" + } + }, + "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "RestApiproxyANY1786B242", + "RestApiproxyC95856DD", + "RestApiANYA7C1DC94" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "RestApiDeploymentStageprod3855DE66": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "ApiAccessLogGroupCEA70788", + "Arn" + ] + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + }, + "DeploymentId": { + "Ref": "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8" + }, + "MethodSettings": [ + { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod" + } + }, + "RestApiproxyC95856DD": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "PathPart": "{proxy+}", + "RestApiId": { + "Ref": "RestApi0C43BF4B" + } + } + }, + "RestApiproxyANYApiPermissiontestcloudfrontapigatewaystackRestApi3EC35350ANYproxy5C73D2AA": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/*/{proxy+}" + ] + ] + } + } + }, + "RestApiproxyANYApiPermissionTesttestcloudfrontapigatewaystackRestApi3EC35350ANYproxy2CC800FF": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/test-invoke-stage/*/{proxy+}" + ] + ] + } + } + }, + "RestApiproxyANY1786B242": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Ref": "RestApiproxyC95856DD" + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "RestApiANYApiPermissiontestcloudfrontapigatewaystackRestApi3EC35350ANYA1A8E51B": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/*/" + ] + ] + } + } + }, + "RestApiANYApiPermissionTesttestcloudfrontapigatewaystackRestApi3EC35350ANYD4888E54": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/test-invoke-stage/*/" + ] + ] + } + } + }, + "RestApiANYA7C1DC94": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "RestApiUsagePlan6E1C537A": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "RestApi0C43BF4B" + }, + "Stage": { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "Throttle": {} + } + ] + } + }, + "ApiAccessLogGroupCEA70788": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LambdaRestApiCloudWatchRoleF339D4E6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "LambdaRestApiAccount": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn" + ] + } + }, + "DependsOn": [ + "RestApi0C43BF4B" + ] + }, + "testcloudfrontapigatewayCloudfrontLoggingBucket9811F6E8": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + }, + { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + } + ] + } + } + }, + "testcloudfrontapigatewayCloudFrontDistributionCFDistribution2270C4C1": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "AllowedMethods": [ + "GET", + "HEAD" + ], + "CachedMethods": [ + "GET", + "HEAD" + ], + "Compress": true, + "ForwardedValues": { + "Cookies": { + "Forward": "none" + }, + "QueryString": false + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https" + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": { + "Bucket": { + "Fn::GetAtt": [ + "testcloudfrontapigatewayCloudfrontLoggingBucket9811F6E8", + "RegionalDomainName" + ] + }, + "IncludeCookies": false + }, + "Origins": [ + { + "CustomOriginConfig": { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginKeepaliveTimeout": 5, + "OriginProtocolPolicy": "https-only", + "OriginReadTimeout": 30, + "OriginSSLProtocols": [ + "TLSv1.2" + ] + }, + "DomainName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "://", + { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "RestApi0C43BF4B" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/" + ] + ] + } + ] + } + ] + } + ] + } + ] + }, + "Id": "origin1" + } + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": { + "CloudFrontDefaultCertificate": true + } + } + } + } + }, + "Parameters": { + "AssetParametersd33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818S3BucketC8A24561": { + "Type": "String", + "Description": "S3 bucket for asset \"d33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818\"" + }, + "AssetParametersd33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818S3VersionKey130FEE10": { + "Type": "String", + "Description": "S3 key for asset version \"d33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818\"" + }, + "AssetParametersd33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818ArtifactHashAC93FA3A": { + "Type": "String", + "Description": "Artifact hash for asset \"d33617a1ef21b93e3004e105c4a9f7b7161a4fb02955f6a28508a80e3c8dc818\"" + } + }, + "Outputs": { + "RestApiEndpoint0551178A": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "RestApi0C43BF4B" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/integ.no-arguments.ts new file mode 100644 index 000000000..d9dffb3cb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/integ.no-arguments.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + + /// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { CloudFrontToApiGateway } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as defaults from '@aws-solutions-konstruk/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-cloudfront-apigateway-stack'); +stack.templateOptions.description = 'Integration Test for aws-cloudfront-apigateway'; + +const inProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' +}; + +const func = defaults.deployLambdaFunction(stack, inProps); + +const _api = defaults.RegionalLambdaRestApi(stack, func); + +new CloudFrontToApiGateway(stack, 'test-cloudfront-apigateway', { + existingApiGatewayObj: _api +}); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/lambda/index.js new file mode 100644 index 000000000..f8b706418 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/lambda/index.js @@ -0,0 +1,23 @@ +/********************************************************************************************************************* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * + * with the License. A copy of the License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * + * and limitations under the License. * + *********************************************************************************************************************/ + +console.log('Loading function'); + +exports.handler = async (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); +    return { +      statusCode: 200, +      headers: { 'Content-Type': 'text/plain' }, +      body: `Hello from Project Vesper! You've hit ${event.path}\n` +    }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/test.cloudfront-apigateway.test.ts b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/test.cloudfront-apigateway.test.ts new file mode 100644 index 000000000..4f73c8375 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-apigateway/test/test.cloudfront-apigateway.test.ts @@ -0,0 +1,160 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils, ResourcePart } from '@aws-cdk/assert'; +import { CloudFrontToApiGateway } from "../lib"; +import * as cdk from "@aws-cdk/core"; +import * as defaults from '@aws-solutions-konstruk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; +import '@aws-cdk/assert/jest'; + +function deploy(stack: cdk.Stack) { + const inProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }; + + const func = defaults.deployLambdaFunction(stack, inProps); + + const _api = defaults.RegionalLambdaRestApi(stack, func); + + return new CloudFrontToApiGateway(stack, 'test-cloudfront-apigateway', { + existingApiGatewayObj: _api + }); +} + +test('snapshot test CloudFrontToApiGateway default params', () => { + const stack = new cdk.Stack(); + deploy(stack); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: CloudFrontToApiGateway = deploy(stack); + + expect(construct.cloudFrontWebDistribution()).toBeDefined(); + expect(construct.restApi()).toBeDefined(); +}); + +test('test cloudfront DomainName', () => { + const stack = new cdk.Stack(); + deploy(stack); + expect(stack).toHaveResourceLike("AWS::CloudFront::Distribution", { + DistributionConfig: { + Origins: [ + { + DomainName: { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "://", + { + "Fn::Join": [ + "", + [ + "https://", + { + Ref: "RestApi0C43BF4B" + }, + ".execute-api.", + { + Ref: "AWS::Region" + }, + ".", + { + Ref: "AWS::URLSuffix" + }, + "/", + { + Ref: "RestApiDeploymentStageprod3855DE66" + }, + "/" + ] + ] + } + ] + } + ] + } + ] + } + ] + } + } + ] + } + }, ResourcePart.Properties); +}); + +test('test api gateway lambda service role', () => { + const stack = new cdk.Stack(); + deploy(stack); + expect(stack).toHaveResource("AWS::IAM::Role", { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "lambda.amazonaws.com" + } + } + ], + Version: "2012-10-17" + }, + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + PolicyName: "LambdaFunctionServiceRolePolicy" + } + ] + }); +}); diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/README.md b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/README.md new file mode 100644 index 000000000..81ef6f5a8 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/README.md @@ -0,0 +1,71 @@ +# aws-cloudfront-s3 module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-cloudfront-s3/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_cloudfront_s3`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-cloudfront-s3`| + +This AWS Solutions Konstruk implements an AWS Cloudfront fronting an AWS S3 Bucket. + +Here is a minimal deployable pattern definition: + +``` javascript +const { CloudFrontToS3 } = require('@aws-solutions-konstruk/aws-cloudfront-s3'); + +new CloudFrontToS3(stack, 'test-cloudfront-s3', { + deployBucket: true +}); + +``` + +## Initializer + +``` text +new CloudFrontToS3(scope: Construct, id: string, props: CloudFrontToS3Props); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`CloudFrontToS3Props`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployBucket|`boolean`|Whether to create a S3 Bucket or use an existing S3 Bucket| +|existingBucketObj?|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Existing instance of S3 Bucket object| +|bucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|Optional user provided props to override the default props for S3 Bucket| +|cloudFrontDistributionProps?|[`cloudfront.CloudFrontWebDistributionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudfront.CloudFrontWebDistributionProps.html)|Optional user provided props to override the default props for Cloudfront Distribution| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|cloudFrontWebDistribution()|[`cloudfront.CloudFrontWebDistribution`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudfront.CloudFrontWebDistribution.html)|Retruns an instance of cloudfront.CloudFrontWebDistribution created by the construct| +|bucket()|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Retruns an instance of s3.Bucket created by the construct| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/architecture.png new file mode 100644 index 000000000..a1721ffb2 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/lib/index.ts new file mode 100644 index 000000000..3c964398c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/lib/index.ts @@ -0,0 +1,95 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as s3 from '@aws-cdk/aws-s3'; +import { Construct } from '@aws-cdk/core'; +import * as defaults from '@aws-solutions-konstruk/core'; + +/** + * @summary The properties for the CloudFrontToS3 Construct + */ +export interface CloudFrontToS3Props { + /** + * Whether to create a S3 Bucket or use an existing S3 Bucket. + * If set to false, you must provide S3 Bucket as `existingBucketObj` + * + * @default - true + */ + readonly deployBucket?: boolean, + /** + * Existing instance of S3 Bucket object. + * If `deployBucket` is set to false only then this property is required + * + * @default - None + */ + readonly existingBucketObj?: s3.Bucket, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly bucketProps?: s3.BucketProps, + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly cloudFrontDistributionProps?: cloudfront.CloudFrontWebDistributionProps; +} + +export class CloudFrontToS3 extends Construct { + private cloudfront: cloudfront.CloudFrontWebDistribution; + private s3Bucket: s3.Bucket; + + /** + * @summary Constructs a new instance of the CloudFrontToS3 class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {CloudFrontToS3Props} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: CloudFrontToS3Props) { + super(scope, id); + + this.s3Bucket = defaults.buildS3Bucket(this, { + deployBucket: props.deployBucket, + existingBucketObj: props.existingBucketObj, + bucketProps: props.bucketProps + }); + + this.cloudfront = defaults.CloudFrontDistributionForS3(this, this.s3Bucket, props.cloudFrontDistributionProps); + } + + /** + * @summary Retruns an instance of cloudfront.CloudFrontWebDistribution created by the construct + * @returns {cloudfront.CloudFrontWebDistribution} Instance of CloudFrontWebDistribution created by the construct + * @since 0.8.0 + * @access public + */ + public cloudFrontWebDistribution(): cloudfront.CloudFrontWebDistribution { + return this.cloudfront; + } + + /** + * @summary Retruns an instance of s3.Bucket created by the construct. + * @returns {s3.Bucket} Instance of Bucket created by the construct + * @since 0.8.0 + * @access public + */ + public bucket(): s3.Bucket { + return this.s3Bucket; + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/package.json b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/package.json new file mode 100644 index 000000000..1e93a17c8 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/package.json @@ -0,0 +1,77 @@ +{ + "name": "@aws-solutions-konstruk/aws-cloudfront-s3", + "version": "0.8.0", + "description": "CDK Constructs for AWS Cloudfront to AWS S3 integration.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.cloudfronts3", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "cloudfronts3" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.CloudfrontS3", + "packageId": "Amazon.Konstruk.AWS.CloudfrontS3", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-cloudfront-s3", + "module": "aws_solutions_konstruk.aws_cloudfront_s3" + } + } + }, + "dependencies": { + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-cloudfront": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-cloudfront": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/test/__snapshots__/test.cloudfront-s3.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/test/__snapshots__/test.cloudfront-s3.test.js.snap new file mode 100644 index 000000000..8c8bbc06a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/test/__snapshots__/test.cloudfront-s3.test.js.snap @@ -0,0 +1,289 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test CloudFrontToS3 default params 1`] = ` +Object { + "Resources": Object { + "testcloudfronts3CloudFrontDistributionCFDistribution61FCC088": Object { + "Properties": Object { + "DistributionConfig": Object { + "DefaultCacheBehavior": Object { + "AllowedMethods": Array [ + "GET", + "HEAD", + ], + "CachedMethods": Array [ + "GET", + "HEAD", + ], + "Compress": true, + "ForwardedValues": Object { + "Cookies": Object { + "Forward": "none", + }, + "QueryString": false, + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https", + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": Object { + "Bucket": Object { + "Fn::GetAtt": Array [ + "testcloudfronts3CloudfrontLoggingBucket985C0FE8", + "RegionalDomainName", + ], + }, + "IncludeCookies": false, + }, + "Origins": Array [ + Object { + "DomainName": Object { + "Fn::GetAtt": Array [ + "testcloudfronts3S3BucketE0C5F76E", + "RegionalDomainName", + ], + }, + "Id": "origin1", + "S3OriginConfig": Object { + "OriginAccessIdentity": Object { + "Fn::Join": Array [ + "", + Array [ + "origin-access-identity/cloudfront/", + Object { + "Ref": "testcloudfronts3CloudFrontOriginAccessIdentity2C681839", + }, + ], + ], + }, + }, + }, + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": Object { + "CloudFrontDefaultCertificate": true, + }, + }, + }, + "Type": "AWS::CloudFront::Distribution", + }, + "testcloudfronts3CloudFrontOriginAccessIdentity2C681839": Object { + "Properties": Object { + "CloudFrontOriginAccessIdentityConfig": Object { + "Comment": "Access S3 bucket content only through CloudFront", + }, + }, + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + }, + "testcloudfronts3CloudfrontLoggingBucket985C0FE8": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + Object { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "testcloudfronts3S3BucketE0C5F76E": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "testcloudfronts3S3LoggingBucket90D239DD", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "testcloudfronts3S3BucketPolicy250F1F61": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "F16", + "reason": "Public website bucket policy requires a wildcard principal", + }, + ], + }, + }, + "Properties": Object { + "Bucket": Object { + "Ref": "testcloudfronts3S3BucketE0C5F76E", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::cloudfront:user/CloudFront Origin Access Identity ", + Object { + "Ref": "testcloudfronts3CloudFrontOriginAccessIdentity2C681839", + }, + ], + ], + }, + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "testcloudfronts3S3BucketE0C5F76E", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "testcloudfronts3S3BucketE0C5F76E", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": "s3:GetObject", + "Effect": "Allow", + "Principal": Object { + "CanonicalUser": Object { + "Fn::GetAtt": Array [ + "testcloudfronts3CloudFrontOriginAccessIdentity2C681839", + "S3CanonicalUserId", + ], + }, + }, + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "testcloudfronts3S3BucketE0C5F76E", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "testcloudfronts3S3LoggingBucket90D239DD": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..98c1260e6 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/test/integ.no-arguments.expected.json @@ -0,0 +1,286 @@ +{ + "Description": "Integration Test for aws-cloudfront-s3", + "Resources": { + "testcloudfronts3S3LoggingBucket90D239DD": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + }, + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "testcloudfronts3S3BucketE0C5F76E": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "testcloudfronts3S3LoggingBucket90D239DD" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testcloudfronts3S3BucketPolicy250F1F61": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "testcloudfronts3S3BucketE0C5F76E" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::cloudfront:user/CloudFront Origin Access Identity ", + { + "Ref": "testcloudfronts3CloudFrontOriginAccessIdentity2C681839" + } + ] + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "testcloudfronts3S3BucketE0C5F76E", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testcloudfronts3S3BucketE0C5F76E", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Principal": { + "CanonicalUser": { + "Fn::GetAtt": [ + "testcloudfronts3CloudFrontOriginAccessIdentity2C681839", + "S3CanonicalUserId" + ] + } + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testcloudfronts3S3BucketE0C5F76E", + "Arn" + ] + }, + "/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "F16", + "reason": "Public website bucket policy requires a wildcard principal" + } + ] + } + } + }, + "testcloudfronts3CloudfrontLoggingBucket985C0FE8": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + }, + { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + } + ] + } + } + }, + "testcloudfronts3CloudFrontOriginAccessIdentity2C681839": { + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + "Properties": { + "CloudFrontOriginAccessIdentityConfig": { + "Comment": "Access S3 bucket content only through CloudFront" + } + } + }, + "testcloudfronts3CloudFrontDistributionCFDistribution61FCC088": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "AllowedMethods": [ + "GET", + "HEAD" + ], + "CachedMethods": [ + "GET", + "HEAD" + ], + "Compress": true, + "ForwardedValues": { + "Cookies": { + "Forward": "none" + }, + "QueryString": false + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https" + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": { + "Bucket": { + "Fn::GetAtt": [ + "testcloudfronts3CloudfrontLoggingBucket985C0FE8", + "RegionalDomainName" + ] + }, + "IncludeCookies": false + }, + "Origins": [ + { + "DomainName": { + "Fn::GetAtt": [ + "testcloudfronts3S3BucketE0C5F76E", + "RegionalDomainName" + ] + }, + "Id": "origin1", + "S3OriginConfig": { + "OriginAccessIdentity": { + "Fn::Join": [ + "", + [ + "origin-access-identity/cloudfront/", + { + "Ref": "testcloudfronts3CloudFrontOriginAccessIdentity2C681839" + } + ] + ] + } + } + } + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": { + "CloudFrontDefaultCertificate": true + } + } + } + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/test/integ.no-arguments.ts new file mode 100644 index 000000000..a9eefadac --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/test/integ.no-arguments.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { CloudFrontToS3, CloudFrontToS3Props } from "../lib"; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-cloudfront-s3-stack'); +stack.templateOptions.description = 'Integration Test for aws-cloudfront-s3'; + +// Definitions +const props: CloudFrontToS3Props = { + deployBucket: true +}; + +new CloudFrontToS3(stack, 'test-cloudfront-s3', props); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/test/test.cloudfront-s3.test.ts b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/test/test.cloudfront-s3.test.ts new file mode 100644 index 000000000..cc86d6f86 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cloudfront-s3/test/test.cloudfront-s3.test.ts @@ -0,0 +1,129 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { CloudFrontToS3, CloudFrontToS3Props } from "../lib"; +import * as cdk from "@aws-cdk/core"; +import * as s3 from '@aws-cdk/aws-s3'; +import '@aws-cdk/assert/jest'; + +function deploy(stack: cdk.Stack) { + const props: CloudFrontToS3Props = { + deployBucket: true + }; + + return new CloudFrontToS3(stack, 'test-cloudfront-s3', props); +} + +test('snapshot test CloudFrontToS3 default params', () => { + const stack = new cdk.Stack(); + deploy(stack); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check s3Bucket default encryption', () => { + const stack = new cdk.Stack(); + deploy(stack); + expect(stack).toHaveResource('AWS::S3::Bucket', { + BucketEncryption: { + ServerSideEncryptionConfiguration: [{ + ServerSideEncryptionByDefault : { + SSEAlgorithm: "AES256" + } + }] + } + }); +}); + +test('check s3Bucket public access block configuration', () => { + const stack = new cdk.Stack(); + deploy(stack); + expect(stack).toHaveResource('AWS::S3::Bucket', { + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true + } + }); +}); + +test('test s3Bucket override publicAccessBlockConfiguration', () => { + const stack = new cdk.Stack(); + + const props: CloudFrontToS3Props = { + deployBucket: true, + bucketProps: { + blockPublicAccess: { + blockPublicAcls: false, + blockPublicPolicy: true, + ignorePublicAcls: false, + restrictPublicBuckets: true + } + } + }; + + new CloudFrontToS3(stack, 'test-cloudfront-s3', props); + + expect(stack).toHaveResource("AWS::S3::Bucket", { + PublicAccessBlockConfiguration: { + BlockPublicAcls: false, + BlockPublicPolicy: true, + IgnorePublicAcls: false, + RestrictPublicBuckets: true + }, + }); +}); + +test('check existing bucket', () => { + const stack = new cdk.Stack(); + + const existingBucket = new s3.Bucket(stack, 'my-bucket', { + bucketName: 'my-bucket' + }); + + const props: CloudFrontToS3Props = { + deployBucket: false, + existingBucketObj: existingBucket + }; + + new CloudFrontToS3(stack, 'test-cloudfront-s3', props); + + expect(stack).toHaveResource("AWS::S3::Bucket", { + BucketName: "my-bucket" + }); + +}); + +test('check exception for Missing existingObj from props for deploy = false', () => { + const stack = new cdk.Stack(); + + const props: CloudFrontToS3Props = { + deployBucket: false + }; + + try { + new CloudFrontToS3(stack, 'test-cloudfront-s3', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); + +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: CloudFrontToS3 = deploy(stack); + + expect(construct.cloudFrontWebDistribution()).toBeDefined(); + expect(construct.bucket()).toBeDefined(); +}); diff --git a/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/README.md b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/README.md new file mode 100644 index 000000000..d74bb723a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/README.md @@ -0,0 +1,83 @@ +# aws-cognito-apigateway-lambda module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-cognito-apigateway-lambda/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_cognito_apigateway_lambda`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-cognito-apigateway-lambda`| + +This AWS Solutions Konstruk implements an Amazon Cognito securing an Amazon API Gateway Lambda backed REST APIs pattern. + +Here is a minimal deployable pattern definition: + +``` javascript +const { CognitoToApiGatewayToLambda } = require('@aws-solutions-konstruk/aws-cognito-apigateway-lambda'); + +const stack = new Stack(app, 'test-cognito-apigateway-lambda-stack'); + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' +}; + +new CognitoToApiGatewayToLambda(stack, 'test-cognito-apigateway-lambda', { + lambdaFunctionProps: lambdaProps, + deployLambda: true +}); +``` + +## Initializer + +``` text +new CognitoToApiGatewayToLambda(scope: Construct, id: string, props: CognitoToApiGatewayToLambdaProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`CognitoToApiGatewayToLambdaProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Existing instance of Lambda Function object| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user provided props to override the default props for Lambda function| +|apiGatewayProps?|[`api.LambdaRestApiProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.LambdaRestApi.html)|Optional user provided props to override the default props for API Gateway| +|cognitoUserPoolProps?|[`cognito.UserPoolProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cognito.UserPoolProps.html)|Optional user provided props to override the default props for Cognito User Pool| +|cognitoUserPoolClientProps?|[`cognito.UserPoolClientProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cognito.UserPoolClientProps.html)|Optional user provided props to override the default props for Cognito User Pool Client| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|restApi()|[`api.RestApi`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigateway.RestApi.html)|Retruns an instance of api.RestApi created by the construct| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Retruns an instance of lambda.Function created by the construct| +|userPool()|[`cognito.UserPool`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cognito.UserPool.html)|Retruns an instance of cognito.UserPool created by the construct| +|userPoolClient()|[`cognito.UserPoolClient`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cognito.UserPoolClient.html)|Retruns an instance of cognito.UserPoolClient created by the construct| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/architecture.png new file mode 100644 index 000000000..69024a03f Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/lib/index.ts new file mode 100644 index 000000000..4d0569169 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/lib/index.ts @@ -0,0 +1,150 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as api from '@aws-cdk/aws-apigateway'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cognito from '@aws-cdk/aws-cognito'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; + +/** + * @summary The properties for the CognitoToApiGatewayToLambda Construct + */ +export interface CognitoToApiGatewayToLambdaProps { + /** + * Whether to create a new Lambda function or use an existing Lambda function. + * If set to false, you must provide a lambda function object as `existingLambdaObj` + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided props to override the default props for the Lambda function. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly lambdaFunctionProps?: lambda.FunctionProps + /** + * Optional user provided props to override the default props for the API Gateway. + * + * @default - Default props are used + */ + readonly apiGatewayProps?: api.LambdaRestApiProps | any + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly cognitoUserPoolProps?: cognito.UserPoolProps + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly cognitoUserPoolClientProps?: cognito.UserPoolClientProps +} + +export class CognitoToApiGatewayToLambda extends Construct { + private userpool: cognito.UserPool; + private userpoolclient: cognito.UserPoolClient; + private api: api.RestApi; + private fn: lambda.Function; + + /** + * @summary Constructs a new instance of the CognitoToApiGatewayToLambda class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {CognitoToApiGatewayToLambdaProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: CognitoToApiGatewayToLambdaProps) { + super(scope, id); + + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + this.api = defaults.GlobalLambdaRestApi(scope, this.fn, props.apiGatewayProps); + this.userpool = defaults.buildUserPool(scope, props.cognitoUserPoolProps); + this.userpoolclient = defaults.buildUserPoolClient(scope, this.userpool, props.cognitoUserPoolClientProps); + + const cfnAuthorizer = new api.CfnAuthorizer(scope, 'CognitoAuthorizer', { + restApiId: this.api.restApiId, + type: 'COGNITO_USER_POOLS', + providerArns: [this.userpool.userPoolArn], + identitySource: "method.request.header.Authorization", + name: "authorizer" + }); + + this.api.methods.forEach((apiMethod) => { + // Leave the authorizer NONE for HTTP OPTIONS method, for the rest set it to COGNITO + const child = apiMethod.node.findChild('Resource') as api.CfnMethod; + if (apiMethod.httpMethod === 'OPTIONS') { + child.addPropertyOverride('AuthorizationType', 'NONE'); + } else { + child.addPropertyOverride('AuthorizationType', 'COGNITO_USER_POOLS'); + child.addPropertyOverride('AuthorizerId', { Ref: cfnAuthorizer.logicalId }); + } + }); + } + + /** + * @summary Retruns an instance of api.RestApi created by the construct. + * @returns {api.RestApi} Instance of RestApi created by the construct + * @since 0.8.0 + * @access public + */ + public restApi(): api.RestApi { + return this.api; + } + + /** + * @summary Retruns an instance of lambda.Function created by the construct. + * @returns {lambda.Function} Instance of Function created by the construct + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } + + /** + * @summary Retruns an instance of cognito.UserPool created by the construct. + * @returns {cognito.UserPool} Instance of UserPool created by the construct + * @since 0.8.0 + * @access public + */ + public userPool(): cognito.UserPool { + return this.userpool; + } + + /** + * @summary Retruns an instance of cognito.UserPoolClient created by the construct. + * @returns {cognito.UserPoolClient} Instance of UserPoolClient created by the construct + * @since 0.8.0 + * @access public + */ + public userPoolClient(): cognito.UserPoolClient { + return this.userpoolclient; + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/package.json b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/package.json new file mode 100644 index 000000000..fae825011 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/package.json @@ -0,0 +1,79 @@ +{ + "name": "@aws-solutions-konstruk/aws-cognito-apigateway-lambda", + "version": "0.8.0", + "description": "CDK Constructs for AWS Cognito to AWS API Gateway to AWS Lambda integration", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.cognitoapigatewaylambda", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "cognitoapigatewaylambda" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.CognitoApigatewayLambda", + "packageId": "Amazon.Konstruk.AWS.CognitoApigatewayLambda", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-cognito-apigateway-lambda", + "module": "aws_solutions_konstruk.aws_cognito_apigateway_lambda" + } + } + }, + "dependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-cognito": "~1.25.0", + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-cognito": "~1.25.0", + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/__snapshots__/test.cognito-apigateway-lambda.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/__snapshots__/test.cognito-apigateway-lambda.test.js.snap new file mode 100644 index 000000000..8ca2165cf --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/__snapshots__/test.cognito-apigateway-lambda.test.js.snap @@ -0,0 +1,620 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test CognitoToApiGatewayToLambda default params 1`] = ` +Object { + "Outputs": Object { + "RestApiEndpoint0551178A": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "RestApi0C43BF4B", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": Object { + "Description": "Artifact hash for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": Object { + "Description": "S3 bucket for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": Object { + "Description": "S3 key for asset version \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + }, + "Resources": Object { + "ApiAccessLogGroupCEA70788": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "CognitoAuthorizer": Object { + "Properties": Object { + "IdentitySource": "method.request.header.Authorization", + "Name": "authorizer", + "ProviderARNs": Array [ + Object { + "Fn::GetAtt": Array [ + "CognitoUserPool53E37E69", + "Arn", + ], + }, + ], + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "Type": "COGNITO_USER_POOLS", + }, + "Type": "AWS::ApiGateway::Authorizer", + }, + "CognitoUserPool53E37E69": Object { + "Properties": Object { + "LambdaConfig": Object {}, + "UserPoolAddOns": Object { + "AdvancedSecurityMode": "ENFORCED", + }, + }, + "Type": "AWS::Cognito::UserPool", + }, + "CognitoUserPoolClient5AB59AE4": Object { + "Properties": Object { + "UserPoolId": Object { + "Ref": "CognitoUserPool53E37E69", + }, + }, + "Type": "AWS::Cognito::UserPoolClient", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaRestApiAccount": Object { + "DependsOn": Array [ + "RestApi0C43BF4B", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "LambdaRestApiCloudWatchRoleF339D4E6": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "RestApi0C43BF4B": Object { + "Properties": Object { + "EndpointConfiguration": Object { + "Types": Array [ + "EDGE", + ], + }, + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "RestApiANYA7C1DC94": Object { + "Properties": Object { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": Object { + "Ref": "CognitoAuthorizer", + }, + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiANYApiPermissionRestApiANY3A99B4EE": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiANYApiPermissionTestRestApiANY79BD91F2": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8": Object { + "DependsOn": Array [ + "RestApiproxyANY1786B242", + "RestApiproxyC95856DD", + "RestApiANYA7C1DC94", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "RestApiDeploymentStageprod3855DE66": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "ApiAccessLogGroupCEA70788", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "RestApiUsagePlan6E1C537A": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "Stage": Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "RestApiproxyANY1786B242": Object { + "Properties": Object { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": Object { + "Ref": "CognitoAuthorizer", + }, + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "RestApiproxyC95856DD", + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiproxyANYApiPermissionRestApiANYproxy9C9912F9": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyANYApiPermissionTestRestApiANYproxyCB7BC56D": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyC95856DD": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..a66b08eae --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/integ.no-arguments.expected.json @@ -0,0 +1,616 @@ +{ + "Resources": { + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "RestApi0C43BF4B": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "RestApiproxyANY1786B242", + "RestApiproxyC95856DD", + "RestApiANYA7C1DC94" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "RestApiDeploymentStageprod3855DE66": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "ApiAccessLogGroupCEA70788", + "Arn" + ] + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + }, + "DeploymentId": { + "Ref": "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8" + }, + "MethodSettings": [ + { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod" + } + }, + "RestApiproxyC95856DD": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "PathPart": "{proxy+}", + "RestApiId": { + "Ref": "RestApi0C43BF4B" + } + } + }, + "RestApiproxyANYApiPermissiontestcognitoapigatewaylambdastackRestApi7173ADDDANYproxyEE672F74": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/*/{proxy+}" + ] + ] + } + } + }, + "RestApiproxyANYApiPermissionTesttestcognitoapigatewaylambdastackRestApi7173ADDDANYproxyF4659A23": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/test-invoke-stage/*/{proxy+}" + ] + ] + } + } + }, + "RestApiproxyANY1786B242": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Ref": "RestApiproxyC95856DD" + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": { + "Ref": "CognitoAuthorizer" + }, + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "RestApiANYApiPermissiontestcognitoapigatewaylambdastackRestApi7173ADDDANY5461DBBA": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/*/" + ] + ] + } + } + }, + "RestApiANYApiPermissionTesttestcognitoapigatewaylambdastackRestApi7173ADDDANYB2D94B09": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/test-invoke-stage/*/" + ] + ] + } + } + }, + "RestApiANYA7C1DC94": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": { + "Ref": "CognitoAuthorizer" + }, + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "RestApiUsagePlan6E1C537A": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "RestApi0C43BF4B" + }, + "Stage": { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "Throttle": {} + } + ] + } + }, + "ApiAccessLogGroupCEA70788": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LambdaRestApiCloudWatchRoleF339D4E6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "LambdaRestApiAccount": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn" + ] + } + }, + "DependsOn": [ + "RestApi0C43BF4B" + ] + }, + "CognitoUserPool53E37E69": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "LambdaConfig": {}, + "UserPoolAddOns": { + "AdvancedSecurityMode": "ENFORCED" + } + } + }, + "CognitoUserPoolClient5AB59AE4": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "CognitoUserPool53E37E69" + } + } + }, + "CognitoAuthorizer": { + "Type": "AWS::ApiGateway::Authorizer", + "Properties": { + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "Type": "COGNITO_USER_POOLS", + "IdentitySource": "method.request.header.Authorization", + "Name": "authorizer", + "ProviderARNs": [ + { + "Fn::GetAtt": [ + "CognitoUserPool53E37E69", + "Arn" + ] + } + ] + } + } + }, + "Parameters": { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": { + "Type": "String", + "Description": "S3 bucket for asset \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": { + "Type": "String", + "Description": "S3 key for asset version \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": { + "Type": "String", + "Description": "Artifact hash for asset \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + } + }, + "Outputs": { + "RestApiEndpoint0551178A": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "RestApi0C43BF4B" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/integ.no-arguments.ts new file mode 100644 index 000000000..dc9513a90 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/integ.no-arguments.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { CognitoToApiGatewayToLambda } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-cognito-apigateway-lambda-stack'); + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' +}; + +new CognitoToApiGatewayToLambda(stack, 'test-cognito-apigateway-lambda', { + lambdaFunctionProps: lambdaProps, + deployLambda: true +}); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/lambda/index.js new file mode 100644 index 000000000..4b3640c1e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/lambda/index.js @@ -0,0 +1,10 @@ +console.log('Loading function'); + +exports.handler = async (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); +    return { +      statusCode: 200, +      headers: { 'Content-Type': 'text/plain' }, +      body: `Hello from Project Vesper! You've hit ${event.path}\n` +    }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/test.cognito-apigateway-lambda.test.ts b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/test.cognito-apigateway-lambda.test.ts new file mode 100644 index 000000000..6beea624a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-cognito-apigateway-lambda/test/test.cognito-apigateway-lambda.test.ts @@ -0,0 +1,96 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { CognitoToApiGatewayToLambda, CognitoToApiGatewayToLambdaProps } from "../lib"; +import * as cdk from "@aws-cdk/core"; +import * as cognito from '@aws-cdk/aws-cognito'; +import * as lambda from '@aws-cdk/aws-lambda'; +import '@aws-cdk/assert/jest'; + +function deployNewFunc(stack: cdk.Stack) { + const lambdaFunctionProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }; + + return new CognitoToApiGatewayToLambda(stack, 'test-cognito-apigateway-lambda', { + deployLambda: true, + lambdaFunctionProps + }); +} + +test('snapshot test CognitoToApiGatewayToLambda default params', () => { + const stack = new cdk.Stack(); + deployNewFunc(stack); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('override cognito properties', () => { + const stack = new cdk.Stack(); + + const lambdaFunctionProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }; + + const cognitoUserPoolProps: cognito.UserPoolProps = { + userPoolName: 'test', + autoVerifiedAttributes: [cognito.UserPoolAttribute.EMAIL] + }; + + new CognitoToApiGatewayToLambda(stack, 'test-cognito-apigateway-lambda', { + deployLambda: true, + lambdaFunctionProps, + cognitoUserPoolProps + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPool', + { + AutoVerifiedAttributes: [ + "email" + ], + LambdaConfig: {}, + UserPoolAddOns: { + AdvancedSecurityMode: "ENFORCED" + }, + UserPoolName: "test" + }); +}); + +test('check exception for Missing existingObj from props for deploy = false', () => { + const stack = new cdk.Stack(); + + const props: CognitoToApiGatewayToLambdaProps = { + deployLambda: false + }; + + try { + new CognitoToApiGatewayToLambda(stack, 'test-cognito-apigateway-lambda', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); + +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: CognitoToApiGatewayToLambda = deployNewFunc(stack); + + expect(construct.userPool()).toBeDefined(); + expect(construct.userPoolClient()).toBeDefined(); + expect(construct.restApi()).toBeDefined(); + expect(construct.lambdaFunction()).toBeDefined(); +}); diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/README.md b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/README.md new file mode 100644 index 000000000..4168da891 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/README.md @@ -0,0 +1,86 @@ +# aws-dynamodb-stream-lambda-elasticsearch-kibana module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-dynamodb-stream-lambda-elasticsearch-kibana/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_dynamodb_stream_elasticsearch_kibana`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana`| + +This AWS Solutions Konstruk implements Amazon DynamoDB table with stream, AWS Lambda function and Amazon Elasticsearch Service with the least privileged permissions. + +Here is a minimal deployable pattern definition: + +``` javascript +const { DynamoDBStreamToLambdaToElasticSearchAndKibana, DynamoDBStreamToLambdaToElasticSearchAndKibanaProps } = require('@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana'); + +const props: DynamoDBStreamToLambdaToElasticSearchAndKibanaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, + domainName: 'test-domain' +}; + +new DynamoDBStreamToLambdaToElasticSearchAndKibana(stack, 'test-dynamodb-stream-lambda-elasticsearch-kibana', props); +``` + +## Initializer + +``` text +new DynamoDBStreamToLambdaToElasticSearchAndKibana(scope: Construct, id: string, props: DynamoDBStreamToLambdaToElasticSearchAndKibanaProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`DynamoDBStreamToLambdaToElasticSearchAndKibanaProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Existing instance of Lambda Function object| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user provided props to override the default props for Lambda function| +|dynamoTableProps?|[`dynamodb.TableProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.TableProps.html)|Optional user provided props to override the default props for DynamoDB Table| +|dynamoEventSourceProps?|[`aws-lambda-event-sources.DynamoEventSourceProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda-event-sources.DynamoEventSourceProps.html)|Optional user provided props to override the default props for DynamoDB Event Source| +|esDomainProps?|[`elasticsearch.CfnDomainProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-elasticsearch.CfnDomainProps.html)|Optional user provided props to override the default props for the Elasticsearch Service| +|domainName|`string`|Domain name for the Cognito and the Elasticsearch Service| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|dynamoTable()|[`dynamodb.Table`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.Table.html)|Retruns an instance of dynamodb.Table created by the construct| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Retruns an instance of lambda.Function created by the construct| +|userPool()|[`cognito.UserPool`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cognito.UserPool.html)|Retruns an instance of cognito.UserPool created by the construct| +|userPoolClient()|[`cognito.UserPoolClient`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cognito.UserPoolClient.html)|Retruns an instance of cognito.UserPoolClient created by the construct| +|identityPool()|[`cognito.CfnIdentityPool`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cognito.CfnIdentityPool.html)|Retruns an instance of cognito.CfnIdentityPool created by the construct| +|elasticsearchDomain()|[`elasticsearch.CfnDomain`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-elasticsearch.CfnDomain.html)|Retruns an instance of elasticsearch.CfnDomain created by the construct| +|cloudwatchAlarms()|[`cloudwatch.Alarm[]`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudwatch.Alarm.html)|Retruns a list of cloudwatch.Alarm created by the construct| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/architecture.png new file mode 100644 index 000000000..84193e88a Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/lib/index.ts new file mode 100644 index 000000000..513a57dd4 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/lib/index.ts @@ -0,0 +1,182 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as lambda from '@aws-cdk/aws-lambda'; +import * as elasticsearch from '@aws-cdk/aws-elasticsearch'; +import { DynamoEventSourceProps } from '@aws-cdk/aws-lambda-event-sources'; +import { DynamoDBStreamToLambdaProps, DynamoDBStreamToLambda } from '@aws-solutions-konstruk/aws-dynamodb-stream-lambda'; +import { LambdaToElasticSearchAndKibanaProps, LambdaToElasticSearchAndKibana } from '@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as cognito from '@aws-cdk/aws-cognito'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { Construct } from '@aws-cdk/core'; + +/** + * @summary The properties for the DynamoDBStreamToLambdaToElastciSearchAndKibana Construct + */ +export interface DynamoDBStreamToLambdaToElasticSearchAndKibanaProps { + /** + * Whether to create a new lambda function or use an existing lambda function. + * If set to false, you must provide a lambda function object as `existingObj` + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly lambdaFunctionProps?: lambda.FunctionProps, + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly dynamoTableProps?: dynamodb.TableProps, + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly dynamoEventSourceProps?: DynamoEventSourceProps, + /** + * Optional user provided props to override the default props for the API Gateway. + * + * @default - Default props are used + */ + readonly esDomainProps?: elasticsearch.CfnDomainProps, + /** + * Cognito & ES Domain Name + * + * @default - None + */ + readonly domainName: string +} + +export class DynamoDBStreamToLambdaToElasticSearchAndKibana extends Construct { + private dynamoDBStreamToLambda: DynamoDBStreamToLambda; + private lambdaToElasticSearchAndKibana: LambdaToElasticSearchAndKibana; + private fn: lambda.Function; + + /** + * @summary Constructs a new instance of the LambdaToDynamoDB class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {DynamoDBStreamToLambdaToElasticSearchAndKibanaProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: DynamoDBStreamToLambdaToElasticSearchAndKibanaProps) { + super(scope, id); + + const _props1: DynamoDBStreamToLambdaProps = { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps, + dynamoEventSourceProps: props.dynamoEventSourceProps, + dynamoTableProps: props.dynamoTableProps + }; + + this.dynamoDBStreamToLambda = new DynamoDBStreamToLambda(scope, 'DynamoDBStreamToLambda', _props1); + + this.fn = this.dynamoDBStreamToLambda.lambdaFunction(); + + const _props2: LambdaToElasticSearchAndKibanaProps = { + deployLambda: false, + existingLambdaObj: this.fn, + domainName: props.domainName, + esDomainProps: props.esDomainProps + }; + + this.lambdaToElasticSearchAndKibana = new LambdaToElasticSearchAndKibana(scope, 'LambdaToElasticSearch', _props2); + } + + /** + * @summary Retruns an instance of dynamodb.Table created by the construct. + * @returns {dynamodb.Table} Instance of dynamodb.Table created by the construct + * @since 0.8.0 + * @access public + */ + public dynamoTable(): dynamodb.Table { + return this.dynamoDBStreamToLambda.dynamoTable(); + } + + /** + * @summary Retruns an instance of lambda.Function created by the construct. + * @returns {lambda.Function} Instance of lambda.Function created by the construct + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.dynamoDBStreamToLambda.lambdaFunction(); + } + + /** + * @summary Retruns an instance of cognito.UserPool created by the construct. + * @returns {cognito.UserPool} Instance of UserPool created by the construct + * @since 0.8.0 + * @access public + */ + public userPool(): cognito.UserPool { + return this.lambdaToElasticSearchAndKibana.userPool(); + } + + /** + * @summary Retruns an instance of cognito.UserPoolClient created by the construct. + * @returns {cognito.UserPoolClient} Instance of UserPoolClient created by the construct + * @since 0.8.0 + * @access public + */ + public userPoolClient(): cognito.UserPoolClient { + return this.lambdaToElasticSearchAndKibana.userPoolClient(); + } + + /** + * @summary Retruns an instance of cognito.CfnIdentityPool created by the construct. + * @returns {cognito.CfnIdentityPool} Instance of CfnIdentityPool created by the construct + * @since 0.8.0 + * @access public + */ + public identityPool(): cognito.CfnIdentityPool { + return this.lambdaToElasticSearchAndKibana.identityPool(); + } + + /** + * @summary Retruns an instance of elasticsearch.CfnDomain created by the construct. + * @returns {elasticsearch.CfnDomain} Instance of CfnDomain created by the construct + * @since 0.8.0 + * @access public + */ + public elasticsearchDomain(): elasticsearch.CfnDomain { + return this.lambdaToElasticSearchAndKibana.elasticsearchDomain(); + } + + /** + * @summary Retruns a list of cloudwatch.Alarm created by the construct. + * @returns {cloudwatch.Alarm[]} List of cloudwatch.Alarm created by the construct + * @since 0.8.0 + * @access public + */ + public cloudwatchAlarms(): cloudwatch.Alarm[] { + return this.lambdaToElasticSearchAndKibana.cloudwatchAlarms(); + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/package.json b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/package.json new file mode 100644 index 000000000..acdb920fd --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/package.json @@ -0,0 +1,89 @@ +{ + "name": "@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana", + "version": "0.8.0", + "description": "CDK Constructs for Amazon Dynamodb stream to AWS Lambda to AWS Elasticsearch with Kibana integration", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.dynamodbstreamlambdaelasticsearchkibana", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "dynamodbstreamlambdaelasticsearchkibana" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.DynamodbStreamLambdaElasticsearchKibana", + "packageId": "Amazon.Konstruk.AWS.DynamodbStreamLambdaElasticsearchKibana", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-dynamodb-stream-lambda-elasticsearch-kibana", + "module": "aws_solutions_konstruk.aws_dynamodb_stream_lambda_elasticsearch_kibana" + } + } + }, + "dependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-lambda-event-sources": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-cognito": "~1.25.0", + "@aws-cdk/aws-elasticsearch": "~1.25.0", + "@aws-cdk/aws-dynamodb": "~1.25.0", + "@aws-cdk/aws-cloudwatch": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-solutions-konstruk/aws-dynamodb-stream-lambda": "~0.8.0", + "@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-cognito": "~1.25.0", + "@aws-cdk/aws-elasticsearch": "~1.25.0", + "@aws-cdk/aws-dynamodb": "~1.25.0", + "@aws-cdk/aws-cloudwatch": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-solutions-konstruk/aws-dynamodb-stream-lambda": "~0.8.0", + "@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana": "~0.8.0", + "@aws-cdk/aws-lambda-event-sources": "~1.25.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/__snapshots__/dynamodb-stream-lambda-elasticsearch-kibana.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/__snapshots__/dynamodb-stream-lambda-elasticsearch-kibana.test.js.snap new file mode 100644 index 000000000..4d657e049 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/__snapshots__/dynamodb-stream-lambda-elasticsearch-kibana.test.js.snap @@ -0,0 +1,681 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test default params 1`] = ` +Object { + "Parameters": Object { + "AssetParameters92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197ArtifactHash052E3F31": Object { + "Description": "Artifact hash for asset \\"92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197\\"", + "Type": "String", + }, + "AssetParameters92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197S3Bucket87AE2D86": Object { + "Description": "S3 bucket for asset \\"92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197\\"", + "Type": "String", + }, + "AssetParameters92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197S3VersionKey6EF53907": Object { + "Description": "S3 key for asset version \\"92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197\\"", + "Type": "String", + }, + }, + "Resources": Object { + "AutomatedSnapshotFailureTooHighAlarmA7918D4F": Object { + "Properties": Object { + "AlarmDescription": "An automated snapshot failed. This failure is often the result of a red cluster health status.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "AutomatedSnapshotFailure", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "CPUUtilizationTooHighAlarmA395C469": Object { + "Properties": Object { + "AlarmDescription": "100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "MetricName": "CPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "CognitoAuthorizedRole14E74FE0": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": Object { + "ForAnyValue:StringLike": Object { + "cognito-identity.amazonaws.com:amr": "authenticated", + }, + "StringEquals": Object { + "cognito-identity.amazonaws.com:aud": Object { + "Ref": "CognitoIdentityPool", + }, + }, + }, + "Effect": "Allow", + "Principal": Object { + "Federated": "cognito-identity.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:es:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":domain/test-domain/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CognitoAccessPolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "CognitoIdentityPool": Object { + "Properties": Object { + "AllowUnauthenticatedIdentities": false, + "CognitoIdentityProviders": Array [ + Object { + "ClientId": Object { + "Ref": "CognitoUserPoolClient5AB59AE4", + }, + "ProviderName": Object { + "Fn::GetAtt": Array [ + "CognitoUserPool53E37E69", + "ProviderName", + ], + }, + "ServerSideTokenCheck": true, + }, + ], + }, + "Type": "AWS::Cognito::IdentityPool", + }, + "CognitoKibanaConfigureRole62CCE76A": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "es.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "CognitoKibanaConfigureRolePolicy76F46A5E": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateElasticsearchDomainConfig", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "CognitoUserPool53E37E69", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:cognito-identity:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":identitypool/", + Object { + "Ref": "CognitoIdentityPool", + }, + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:es:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":domain/test-domain", + ], + ], + }, + ], + }, + Object { + "Action": "iam:PassRole", + "Condition": Object { + "StringLike": Object { + "iam:PassedToService": "cognito-identity.amazonaws.com", + }, + }, + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CognitoKibanaConfigureRolePolicy76F46A5E", + "Roles": Array [ + Object { + "Ref": "CognitoKibanaConfigureRole62CCE76A", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "CognitoUserPool53E37E69": Object { + "Properties": Object { + "LambdaConfig": Object {}, + "UserPoolAddOns": Object { + "AdvancedSecurityMode": "ENFORCED", + }, + }, + "Type": "AWS::Cognito::UserPool", + }, + "CognitoUserPoolClient5AB59AE4": Object { + "Properties": Object { + "UserPoolId": Object { + "Ref": "CognitoUserPool53E37E69", + }, + }, + "Type": "AWS::Cognito::UserPoolClient", + }, + "DynamoDBStreamToLambdaDynamoTable900492E8": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "AttributeDefinitions": Array [ + Object { + "AttributeName": "id", + "AttributeType": "S", + }, + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": Array [ + Object { + "AttributeName": "id", + "KeyType": "HASH", + }, + ], + "SSESpecification": Object { + "SSEEnabled": true, + }, + "StreamSpecification": Object { + "StreamViewType": "NEW_AND_OLD_IMAGES", + }, + }, + "Type": "AWS::DynamoDB::Table", + "UpdateReplacePolicy": "Retain", + }, + "ElasticsearchDomain": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W28", + "reason": "The ES Domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific ES instance only", + }, + ], + }, + }, + "Properties": Object { + "AccessPolicies": Object { + "Statement": Array [ + Object { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Principal": Object { + "AWS": Array [ + Object { + "Fn::GetAtt": Array [ + "CognitoAuthorizedRole14E74FE0", + "Arn", + ], + }, + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + ], + }, + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:es:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":domain/test-domain/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "CognitoOptions": Object { + "Enabled": true, + "IdentityPoolId": Object { + "Ref": "CognitoIdentityPool", + }, + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn", + ], + }, + "UserPoolId": Object { + "Ref": "CognitoUserPool53E37E69", + }, + }, + "DomainName": "test-domain", + "EBSOptions": Object { + "EBSEnabled": true, + "VolumeSize": 10, + }, + "ElasticsearchClusterConfig": Object { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "InstanceCount": 3, + "ZoneAwarenessConfig": Object { + "AvailabilityZoneCount": 3, + }, + "ZoneAwarenessEnabled": true, + }, + "ElasticsearchVersion": "6.3", + "EncryptionAtRestOptions": Object { + "Enabled": true, + }, + "NodeToNodeEncryptionOptions": Object { + "Enabled": true, + }, + "SnapshotOptions": Object { + "AutomatedSnapshotStartHour": 1, + }, + }, + "Type": "AWS::Elasticsearch::Domain", + }, + "FreeStorageSpaceTooLowAlarm3410CBE2": Object { + "Properties": Object { + "AlarmDescription": "A node in your cluster is down to 20 GiB of free storage space.", + "ComparisonOperator": "LessThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "FreeStorageSpace", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Minimum", + "Threshold": 2000, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "IdentityPoolRoleMapping": Object { + "Properties": Object { + "IdentityPoolId": Object { + "Ref": "CognitoIdentityPool", + }, + "Roles": Object { + "authenticated": Object { + "Fn::GetAtt": Array [ + "CognitoAuthorizedRole14E74FE0", + "Arn", + ], + }, + }, + }, + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + }, + "IndexWritesBlockedTooHighAlarm5F7E9A55": Object { + "Properties": Object { + "AlarmDescription": "Your cluster is blocking write requests.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "ClusterIndexWritesBlocked", + "Namespace": "AWS/ES", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "JVMMemoryPressureTooHighAlarm303EEA7C": Object { + "Properties": Object { + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "JVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197S3Bucket87AE2D86", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197S3VersionKey6EF53907", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197S3VersionKey6EF53907", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DOMAIN_ENDPOINT": Object { + "Fn::GetAtt": Array [ + "ElasticsearchDomain", + "DomainEndpoint", + ], + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionDynamoDBEventSourceDynamoDBStreamToLambdaDynamoTableDA56B777DD1406A9": Object { + "Properties": Object { + "BatchSize": 100, + "EventSourceArn": Object { + "Fn::GetAtt": Array [ + "DynamoDBStreamToLambdaDynamoTable900492E8", + "StreamArn", + ], + }, + "FunctionName": Object { + "Ref": "LambdaFunctionBF21E41F", + }, + "StartingPosition": "TRIM_HORIZON", + }, + "Type": "AWS::Lambda::EventSourceMapping", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "dynamodb:ListStreams", + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "DynamoDBStreamToLambdaDynamoTable900492E8", + "Arn", + ], + }, + "/stream/*", + ], + ], + }, + }, + Object { + "Action": Array [ + "dynamodb:DescribeStream", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "DynamoDBStreamToLambdaDynamoTable900492E8", + "StreamArn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "MasterCPUUtilizationTooHighAlarm1CE1084B": Object { + "Properties": Object { + "AlarmDescription": "Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "MetricName": "MasterCPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "MasterJVMMemoryPressureTooHighAlarmBB15F770": Object { + "Properties": Object { + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "MasterJVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "StatusRedAlarm4CE918C2": Object { + "Properties": Object { + "AlarmDescription": "At least one primary shard and its replicas are not allocated to a node. ", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "ClusterStatus.red", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "StatusYellowAlarm2B20F083": Object { + "Properties": Object { + "AlarmDescription": "At least one replica shard is not allocated to a node.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "ClusterStatus.yellow", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "UserPoolDomain": Object { + "DependsOn": Array [ + "CognitoUserPool53E37E69", + ], + "Properties": Object { + "Domain": "test-domain", + "UserPoolId": Object { + "Ref": "CognitoUserPool53E37E69", + }, + }, + "Type": "AWS::Cognito::UserPoolDomain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/dynamodb-stream-lambda-elasticsearch-kibana.test.ts b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/dynamodb-stream-lambda-elasticsearch-kibana.test.ts new file mode 100644 index 000000000..f5bb58637 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/dynamodb-stream-lambda-elasticsearch-kibana.test.ts @@ -0,0 +1,88 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { DynamoDBStreamToLambdaToElasticSearchAndKibana, DynamoDBStreamToLambdaToElasticSearchAndKibanaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as cdk from "@aws-cdk/core"; +import { CfnDomain } from '@aws-cdk/aws-elasticsearch'; +import { CfnIdentityPool, UserPool, UserPoolClient } from '@aws-cdk/aws-cognito'; +import '@aws-cdk/assert/jest'; + +function deployNewFunc(stack: cdk.Stack) { + const props: DynamoDBStreamToLambdaToElasticSearchAndKibanaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }, + domainName: 'test-domain' + }; + + return new DynamoDBStreamToLambdaToElasticSearchAndKibana(stack, 'test-dynamodb-stream-lambda-elasticsearch-stack', props); +} + +test('snapshot test default params', () => { + const stack = new cdk.Stack(); + deployNewFunc(stack); + + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check domain names', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolDomain', { + Domain: "test-domain", + UserPoolId: { + Ref: "CognitoUserPool53E37E69" + } + }); + + expect(stack).toHaveResource('AWS::Elasticsearch::Domain', { + DomainName: "test-domain", + }); +}); + +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: DynamoDBStreamToLambdaToElasticSearchAndKibana = deployNewFunc(stack); + + expect(construct.lambdaFunction()).toBeInstanceOf(lambda.Function); + expect(construct.dynamoTable()).toBeInstanceOf(dynamodb.Table); + expect(construct.elasticsearchDomain()).toBeInstanceOf(CfnDomain); + expect(construct.identityPool()).toBeInstanceOf(CfnIdentityPool); + expect(construct.userPool()).toBeInstanceOf(UserPool); + expect(construct.userPoolClient()).toBeInstanceOf(UserPoolClient); + expect(construct.cloudwatchAlarms()).toHaveLength(9); +}); + +test('check exception for Missing existingObj from props for deploy = false', () => { + const stack = new cdk.Stack(); + + const props: DynamoDBStreamToLambdaToElasticSearchAndKibanaProps = { + deployLambda: true, + domainName: 'test-domain' + }; + + try { + new DynamoDBStreamToLambdaToElasticSearchAndKibana(stack, 'test-lambda-elasticsearch-stack', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..fb78c8e33 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/integ.no-arguments.expected.json @@ -0,0 +1,677 @@ +{ + "Resources": { + "DynamoDBStreamToLambdaDynamoTable900492E8": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "SSESpecification": { + "SSEEnabled": true + }, + "StreamSpecification": { + "StreamViewType": "NEW_AND_OLD_IMAGES" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:ListStreams", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "DynamoDBStreamToLambdaDynamoTable900492E8", + "Arn" + ] + }, + "/stream/*" + ] + ] + } + }, + { + "Action": [ + "dynamodb:DescribeStream", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "DynamoDBStreamToLambdaDynamoTable900492E8", + "StreamArn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197S3Bucket87AE2D86" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197S3VersionKey6EF53907" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197S3VersionKey6EF53907" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DOMAIN_ENDPOINT": { + "Fn::GetAtt": [ + "ElasticsearchDomain", + "DomainEndpoint" + ] + } + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "LambdaFunctionDynamoDBEventSourcetestdynamodbstreamlambdaelasticsearchkibanastackDynamoDBStreamToLambdaDynamoTableF3692FDE826139F4": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "EventSourceArn": { + "Fn::GetAtt": [ + "DynamoDBStreamToLambdaDynamoTable900492E8", + "StreamArn" + ] + }, + "FunctionName": { + "Ref": "LambdaFunctionBF21E41F" + }, + "BatchSize": 100, + "StartingPosition": "TRIM_HORIZON" + } + }, + "CognitoUserPool53E37E69": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "LambdaConfig": {}, + "UserPoolAddOns": { + "AdvancedSecurityMode": "ENFORCED" + } + } + }, + "CognitoUserPoolClient5AB59AE4": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "CognitoUserPool53E37E69" + } + } + }, + "CognitoIdentityPool": { + "Type": "AWS::Cognito::IdentityPool", + "Properties": { + "AllowUnauthenticatedIdentities": false, + "CognitoIdentityProviders": [ + { + "ClientId": { + "Ref": "CognitoUserPoolClient5AB59AE4" + }, + "ProviderName": { + "Fn::GetAtt": [ + "CognitoUserPool53E37E69", + "ProviderName" + ] + }, + "ServerSideTokenCheck": true + } + ] + } + }, + "UserPoolDomain": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "myvesperdomain", + "UserPoolId": { + "Ref": "CognitoUserPool53E37E69" + } + }, + "DependsOn": [ + "CognitoUserPool53E37E69" + ] + }, + "CognitoAuthorizedRole14E74FE0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "CognitoIdentityPool" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "authenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/myvesperdomain/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CognitoAccessPolicy" + } + ] + } + }, + "IdentityPoolRoleMapping": { + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + "Properties": { + "IdentityPoolId": { + "Ref": "CognitoIdentityPool" + }, + "Roles": { + "authenticated": { + "Fn::GetAtt": [ + "CognitoAuthorizedRole14E74FE0", + "Arn" + ] + } + } + } + }, + "CognitoKibanaConfigureRole62CCE76A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "es.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CognitoKibanaConfigureRolePolicy76F46A5E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateElasticsearchDomainConfig" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "CognitoUserPool53E37E69", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:cognito-identity:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":identitypool/", + { + "Ref": "CognitoIdentityPool" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/myvesperdomain" + ] + ] + } + ] + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringLike": { + "iam:PassedToService": "cognito-identity.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CognitoKibanaConfigureRolePolicy76F46A5E", + "Roles": [ + { + "Ref": "CognitoKibanaConfigureRole62CCE76A" + } + ] + } + }, + "ElasticsearchDomain": { + "Type": "AWS::Elasticsearch::Domain", + "Properties": { + "AccessPolicies": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Principal": { + "AWS": [ + { + "Fn::GetAtt": [ + "CognitoAuthorizedRole14E74FE0", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + } + ] + }, + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/myvesperdomain/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "CognitoOptions": { + "Enabled": true, + "IdentityPoolId": { + "Ref": "CognitoIdentityPool" + }, + "RoleArn": { + "Fn::GetAtt": [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn" + ] + }, + "UserPoolId": { + "Ref": "CognitoUserPool53E37E69" + } + }, + "DomainName": "myvesperdomain", + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10 + }, + "ElasticsearchClusterConfig": { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "InstanceCount": 3, + "ZoneAwarenessConfig": { + "AvailabilityZoneCount": 3 + }, + "ZoneAwarenessEnabled": true + }, + "ElasticsearchVersion": "6.3", + "EncryptionAtRestOptions": { + "Enabled": true + }, + "NodeToNodeEncryptionOptions": { + "Enabled": true + }, + "SnapshotOptions": { + "AutomatedSnapshotStartHour": 1 + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W28", + "reason": "The ES Domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific ES instance only" + } + ] + } + } + }, + "StatusRedAlarm4CE918C2": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one primary shard and its replicas are not allocated to a node. ", + "MetricName": "ClusterStatus.red", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "StatusYellowAlarm2B20F083": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one replica shard is not allocated to a node.", + "MetricName": "ClusterStatus.yellow", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "FreeStorageSpaceTooLowAlarm3410CBE2": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "LessThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "A node in your cluster is down to 20 GiB of free storage space.", + "MetricName": "FreeStorageSpace", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Minimum", + "Threshold": 2000 + } + }, + "IndexWritesBlockedTooHighAlarm5F7E9A55": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Your cluster is blocking write requests.", + "MetricName": "ClusterIndexWritesBlocked", + "Namespace": "AWS/ES", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "AutomatedSnapshotFailureTooHighAlarmA7918D4F": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "An automated snapshot failed. This failure is often the result of a red cluster health status.", + "MetricName": "AutomatedSnapshotFailure", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "CPUUtilizationTooHighAlarmA395C469": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.", + "MetricName": "CPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "JVMMemoryPressureTooHighAlarm303EEA7C": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "JVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "MasterCPUUtilizationTooHighAlarm1CE1084B": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.", + "MetricName": "MasterCPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "MasterJVMMemoryPressureTooHighAlarmBB15F770": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "MasterJVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + } + }, + "Parameters": { + "AssetParameters92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197S3Bucket87AE2D86": { + "Type": "String", + "Description": "S3 bucket for asset \"92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197\"" + }, + "AssetParameters92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197S3VersionKey6EF53907": { + "Type": "String", + "Description": "S3 key for asset version \"92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197\"" + }, + "AssetParameters92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197ArtifactHash052E3F31": { + "Type": "String", + "Description": "Artifact hash for asset \"92927de5fcc3aea277bddecb845bee318fb502f7375daedbdafb72c0400bc197\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/integ.no-arguments.ts new file mode 100644 index 000000000..d34bf0dce --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/integ.no-arguments.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { DynamoDBStreamToLambdaToElasticSearchAndKibanaProps, DynamoDBStreamToLambdaToElasticSearchAndKibana } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +const app = new App(); + +// Empty arguments +const stack = new Stack(app, 'test-dynamodb-stream-lambda-elasticsearch-kibana-stack'); + +const props: DynamoDBStreamToLambdaToElasticSearchAndKibanaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, + domainName: 'myvesperdomain' +}; + +new DynamoDBStreamToLambdaToElasticSearchAndKibana(stack, 'test-dynamodb-stream-lambda-elasticsearch-kibana', props); +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/lambda/index.js new file mode 100644 index 000000000..6d89b241c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda-elasticsearch-kibana/test/lambda/index.js @@ -0,0 +1,68 @@ +var AWS = require('aws-sdk'); +var path = require('path'); + +console.log('Loading function'); + +var esDomain = { + endpoint: process.env.DOMAIN_ENDPOINT, + region: process.env.AWS_REGION, + index: 'lambda-index', + doctype: 'lambda-type' +}; + +var creds = new AWS.EnvironmentCredentials('AWS'); +var endpoint = new AWS.Endpoint(esDomain.endpoint); + +function postDocumentToES(doc, context, id) { + var req = new AWS.HttpRequest(endpoint); + + req.method = 'POST'; + req.path = path.join('/', esDomain.index, esDomain.doctype, id); + req.region = esDomain.region; + req.body = doc; + req.headers['presigned-expires'] = false; + req.headers['Host'] = esDomain.endpoint; + req.headers['Content-Type'] = 'application/json'; + + // Sign the request (Sigv4) + var signer = new AWS.Signers.V4(req, 'es'); + signer.addAuthorization(creds, new Date()); + + // Post document to ES + var send = new AWS.NodeHttpClient(); + send.handleRequest(req, null, function(httpResp) { + var body = ''; + httpResp.on('data', function (chunk) { + body += chunk; + }); + httpResp.on('end', function (chunk) { + console.log('DynamoDB record added to ES.'); + context.succeed(); + }); + }, function(err) { + console.log('Error: ' + err); + context.fail(); + }); +} + +exports.handler = (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); + let count = 0; + + event.Records.forEach((record) => { + const id = record.dynamodb.Keys.id.S; + + if (record.dynamodb.NewImage) { + postDocumentToES(JSON.stringify(record.dynamodb.NewImage), context, id); + } + + count += 1 + }); + + + return { +      statusCode: 200, +      headers: { 'Content-Type': 'text/plain' }, +      body: `${count} records processed.\n` +    }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/README.md b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/README.md new file mode 100644 index 000000000..551786f07 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/README.md @@ -0,0 +1,79 @@ +# aws-dynamodb-stream-lambda module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-dynamodb-stream-lambda/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_dynamodb_stream_lambda`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-dynamodb-stream-lambda`| + +This AWS Solutions Konstruk implements a pattern Amazon DynamoDB table with stream to invoke the AWS Lambda function with the least privileged permissions. + +Here is a minimal deployable pattern definition: + +``` javascript +const { DynamoDBStreamToLambdaProps, DynamoDBStreamToLambda} = require('@aws-solutions-konstruk/aws-dynamodb-stream-lambda'); + +const props: DynamoDBStreamToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, +}; + +new DynamoDBStreamToLambda(stack, 'test-dynamodb-stream-lambda', props); + +``` + +## Initializer + +``` text +new DynamoDBStreamToLambda(scope: Construct, id: string, props: DynamoDBStreamToLambdaProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`DynamoDBStreamToLambdaProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Existing instance of Lambda Function object| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user provided props to override the default props for Lambda function| +|dynamoTableProps?|[`dynamodb.TableProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.TableProps.html)|Optional user provided props to override the default props for DynamoDB Table| +|dynamoEventSourceProps?|[`aws-lambda-event-sources.DynamoEventSourceProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda-event-sources.DynamoEventSourceProps.html)|Optional user provided props to override the default props for DynamoDB Event Source| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|dynamoTable()|[`dynamodb.Table`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.Table.html)|Retruns an instance of dynamodb.Table created by the construct| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Retruns an instance of lambda.Function created by the construct| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/architecture.png new file mode 100644 index 000000000..ab5382db3 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/lib/index.ts new file mode 100644 index 000000000..b5ed15ea8 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/lib/index.ts @@ -0,0 +1,117 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as lambda from '@aws-cdk/aws-lambda'; +import { DynamoEventSourceProps, DynamoEventSource } from '@aws-cdk/aws-lambda-event-sources'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; +import { overrideProps } from '@aws-solutions-konstruk/core'; + +/** + * @summary The properties for the DynamoDBStreamToLambda Construct + */ +export interface DynamoDBStreamToLambdaProps { + /** + * Whether to create a new lambda function or use an existing lambda function. + * If set to false, you must provide a lambda function object as `existingObj` + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly lambdaFunctionProps?: lambda.FunctionProps, + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly dynamoTableProps?: dynamodb.TableProps, + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly dynamoEventSourceProps?: DynamoEventSourceProps +} + +export class DynamoDBStreamToLambda extends Construct { + private fn: lambda.Function; + private table: dynamodb.Table; + + /** + * @summary Constructs a new instance of the LambdaToDynamoDB class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {DynamoDBStreamToLambdaProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: DynamoDBStreamToLambdaProps) { + super(scope, id); + + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + + // Set the default props for DynamoDB table + if (props.dynamoTableProps) { + const dynamoTableProps = overrideProps(defaults.DefaultTableWithStreamProps, props.dynamoTableProps); + this.table = new dynamodb.Table(this, 'DynamoTable', dynamoTableProps); + } else { + this.table = new dynamodb.Table(this, 'DynamoTable', defaults.DefaultTableWithStreamProps); + } + + // Grant DynamoDB Stream read perimssion for lambda function + this.table.grantStreamRead(this.fn.grantPrincipal); + + // Create DynamDB trigger to invoke lambda function + this.fn.addEventSource(new DynamoEventSource(this.table, + defaults.DynamoEventSourceProps(props.dynamoEventSourceProps))); + } + + /** + * @summary Retruns an instance of dynamodb.Table created by the construct. + * @returns {dynamodb.Table} Instance of dynamodb.Table created by the construct + * @since 0.8.0 + * @access public + */ + public dynamoTable(): dynamodb.Table { + return this.table; + } + + /** + * @summary Retruns an instance of lambda.Function created by the construct. + * @returns {lambda.Function} Instance of lambda.Function created by the construct + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } + +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/package.json b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/package.json new file mode 100644 index 000000000..5490e69aa --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/package.json @@ -0,0 +1,79 @@ +{ + "name": "@aws-solutions-konstruk/aws-dynamodb-stream-lambda", + "version": "0.8.0", + "description": "CDK Constructs for AWS DynamoDB Stream to AWS Lambda integration.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.dynamodbstreamlambda", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "dynamodbstreamlambda" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.DynamodbStreamLambda", + "packageId": "Amazon.Konstruk.AWS.DynamodbStreamLambda", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-dynamodb-stream-lambda", + "module": "aws_solutions_konstruk.aws_dynamodb_stream_lambda" + } + } + }, + "dependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-lambda-event-sources": "~1.25.0", + "@aws-cdk/aws-dynamodb": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-dynamodb": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-cdk/aws-lambda-event-sources": "~1.25.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/__snapshots__/dynamodb-stream-lambda.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/__snapshots__/dynamodb-stream-lambda.test.js.snap new file mode 100644 index 000000000..5a97cea2a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/__snapshots__/dynamodb-stream-lambda.test.js.snap @@ -0,0 +1,233 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test DynamoDBStreamToLambda default params 1`] = ` +Object { + "Parameters": Object { + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8ArtifactHash8D9AD644": Object { + "Description": "Artifact hash for asset \\"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\\"", + "Type": "String", + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB": Object { + "Description": "S3 bucket for asset \\"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\\"", + "Type": "String", + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7": Object { + "Description": "S3 key for asset version \\"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionDynamoDBEventSourcetestlambdadynamodbstackDynamoTableD6E2BCEF52F21754": Object { + "Properties": Object { + "BatchSize": 100, + "EventSourceArn": Object { + "Fn::GetAtt": Array [ + "testlambdadynamodbstackDynamoTable8138E93B", + "StreamArn", + ], + }, + "FunctionName": Object { + "Ref": "LambdaFunctionBF21E41F", + }, + "StartingPosition": "TRIM_HORIZON", + }, + "Type": "AWS::Lambda::EventSourceMapping", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "dynamodb:ListStreams", + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "testlambdadynamodbstackDynamoTable8138E93B", + "Arn", + ], + }, + "/stream/*", + ], + ], + }, + }, + Object { + "Action": Array [ + "dynamodb:DescribeStream", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "testlambdadynamodbstackDynamoTable8138E93B", + "StreamArn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "testlambdadynamodbstackDynamoTable8138E93B": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "AttributeDefinitions": Array [ + Object { + "AttributeName": "id", + "AttributeType": "S", + }, + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": Array [ + Object { + "AttributeName": "id", + "KeyType": "HASH", + }, + ], + "SSESpecification": Object { + "SSEEnabled": true, + }, + "StreamSpecification": Object { + "StreamViewType": "NEW_AND_OLD_IMAGES", + }, + }, + "Type": "AWS::DynamoDB::Table", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/dynamodb-stream-lambda.test.ts b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/dynamodb-stream-lambda.test.ts new file mode 100644 index 000000000..d56861454 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/dynamodb-stream-lambda.test.ts @@ -0,0 +1,206 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { DynamoDBStreamToLambda, DynamoDBStreamToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as cdk from "@aws-cdk/core"; +import '@aws-cdk/assert/jest'; + +function deployNewFunc(stack: cdk.Stack) { + const props: DynamoDBStreamToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, + }; + + return new DynamoDBStreamToLambda(stack, 'test-lambda-dynamodb-stack', props); +} + +test('snapshot test DynamoDBStreamToLambda default params', () => { + const stack = new cdk.Stack(); + deployNewFunc(stack); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check lambda EventSourceMapping', () => { + const stack = new cdk.Stack(); + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::Lambda::EventSourceMapping', { + EventSourceArn: { + "Fn::GetAtt": [ + "testlambdadynamodbstackDynamoTable8138E93B", + "StreamArn" + ] + }, + FunctionName: { + Ref: "LambdaFunctionBF21E41F" + }, + BatchSize: 100, + StartingPosition: "TRIM_HORIZON" + }); +}); + +test('check DynamoEventSourceProps override', () => { + const stack = new cdk.Stack(); + const props: DynamoDBStreamToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, + dynamoEventSourceProps: { + startingPosition: lambda.StartingPosition.LATEST, + batchSize: 55 + } + }; + + new DynamoDBStreamToLambda(stack, 'test-lambda-dynamodb-stack', props); + + expect(stack).toHaveResource('AWS::Lambda::EventSourceMapping', { + EventSourceArn: { + "Fn::GetAtt": [ + "testlambdadynamodbstackDynamoTable8138E93B", + "StreamArn" + ] + }, + FunctionName: { + Ref: "LambdaFunctionBF21E41F" + }, + BatchSize: 55, + StartingPosition: "LATEST" + }); +}); + +test('check lambda permission to read dynamodb stream', () => { + const stack = new cdk.Stack(); + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: "dynamodb:ListStreams", + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testlambdadynamodbstackDynamoTable8138E93B", + "Arn" + ] + }, + "/stream/*" + ] + ] + } + }, + { + Action: [ + "dynamodb:DescribeStream", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator" + ], + Effect: "Allow", + Resource: { + "Fn::GetAtt": [ + "testlambdadynamodbstackDynamoTable8138E93B", + "StreamArn" + ] + } + } + ], + Version: "2012-10-17" + }, + PolicyName: "LambdaFunctionServiceRoleDefaultPolicy126C8897", + Roles: [ + { + Ref: "LambdaFunctionServiceRole0C4CDE0B" + } + ] + }); +}); + +test('check dynamodb table stream override', () => { + const stack = new cdk.Stack(); + const props: DynamoDBStreamToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, + dynamoTableProps: { + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + }, + stream: dynamodb.StreamViewType.NEW_IMAGE + } + }; + + new DynamoDBStreamToLambda(stack, 'test-lambda-dynamodb-stack', props); + expect(stack).toHaveResource('AWS::DynamoDB::Table', { + KeySchema: [ + { + AttributeName: "id", + KeyType: "HASH" + } + ], + AttributeDefinitions: [ + { + AttributeName: "id", + AttributeType: "S" + } + ], + BillingMode: "PAY_PER_REQUEST", + SSESpecification: { + SSEEnabled: true + }, + StreamSpecification: { + StreamViewType: "NEW_IMAGE" + } + }); + +}); + +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: DynamoDBStreamToLambda = deployNewFunc(stack); + + expect(construct.lambdaFunction()).toBeInstanceOf(lambda.Function); + expect(construct.dynamoTable()).toBeInstanceOf(dynamodb.Table); +}); + +test('check exception for Missing existingObj from props for deploy = false', () => { + const stack = new cdk.Stack(); + + const props: DynamoDBStreamToLambdaProps = { + deployLambda: true + }; + + try { + new DynamoDBStreamToLambda(stack, 'test-iot-lambda-integration', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..f8b01d1c1 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/integ.no-arguments.expected.json @@ -0,0 +1,229 @@ +{ + "Resources": { + "testdynamodbstreamlambdaDynamoTable6EB2ED0F": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "SSESpecification": { + "SSEEnabled": true + }, + "StreamSpecification": { + "StreamViewType": "NEW_AND_OLD_IMAGES" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:ListStreams", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testdynamodbstreamlambdaDynamoTable6EB2ED0F", + "Arn" + ] + }, + "/stream/*" + ] + ] + } + }, + { + "Action": [ + "dynamodb:DescribeStream", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testdynamodbstreamlambdaDynamoTable6EB2ED0F", + "StreamArn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "LambdaFunctionDynamoDBEventSourcetestdynamodbstreamlambdastacktestdynamodbstreamlambdaDynamoTable99DF98248D8C4DDC": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "EventSourceArn": { + "Fn::GetAtt": [ + "testdynamodbstreamlambdaDynamoTable6EB2ED0F", + "StreamArn" + ] + }, + "FunctionName": { + "Ref": "LambdaFunctionBF21E41F" + }, + "BatchSize": 100, + "StartingPosition": "TRIM_HORIZON" + } + } + }, + "Parameters": { + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB": { + "Type": "String", + "Description": "S3 bucket for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7": { + "Type": "String", + "Description": "S3 key for asset version \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8ArtifactHash8D9AD644": { + "Type": "String", + "Description": "Artifact hash for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/integ.no-arguments.ts new file mode 100644 index 000000000..d50708b3c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/integ.no-arguments.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { DynamoDBStreamToLambdaProps, DynamoDBStreamToLambda } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +const app = new App(); + +// Empty arguments +const stack = new Stack(app, 'test-dynamodb-stream-lambda-stack'); + +const props: DynamoDBStreamToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, +}; + +new DynamoDBStreamToLambda(stack, 'test-dynamodb-stream-lambda', props); +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/lambda/index.js new file mode 100644 index 000000000..743e4fdbb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-dynamodb-stream-lambda/test/lambda/index.js @@ -0,0 +1,8 @@ +exports.handler = async function(event) { + console.log('request:', JSON.stringify(event, undefined, 2)); + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: `Hello, CDK! You've hit ${event.path}\n` + }; + }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/README.md b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/README.md new file mode 100644 index 000000000..bab9cdad3 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/README.md @@ -0,0 +1,80 @@ +# aws-events-rule-lambda module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-events-rule-lambda/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_events_rule_lambda`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-events-rule-lambda`| + +This AWS Solutions Konstruk implements an AWS Events rule and an AWS Lambda function. + +Here is a minimal deployable pattern definition: + +``` javascript +const { EventsRuleToLambdaProps, EventsRuleToLambda } = require('@aws-solutions-konstruk/aws-events-rule-lambda'); + +const props: EventsRuleToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, + eventRuleProps: { + schedule: events.Schedule.rate(Duration.minutes(5)) + } +}; + +new EventsRuleToLambda(stack, 'test-events-rule-lambda', props); +``` + +## Initializer + +``` text +new EventsRuleToLambda(scope: Construct, id: string, props: EventsRuleToLambdaProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`EventsRuleToLambdaProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new lambda function or use an existing lambda function| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Existing instance of Lambda Function object| +|lambdaFunctionProps|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user provided props to override the default props for lambda.Function| +|eventRuleProps|[`events.RuleProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-events.RuleProps.html)|User provided eventRuleProps to override the defaults| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|eventsRule()|[`events.Rule`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-events.Rule.html)|Retruns an instance of events.Rule created by the construct| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Retruns an instance of lambda.Function created by the construct| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/architecture.png new file mode 100644 index 000000000..cab5375d6 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/lib/index.ts new file mode 100644 index 000000000..037d09f4e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/lib/index.ts @@ -0,0 +1,112 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as lambda from '@aws-cdk/aws-lambda'; +import * as events from '@aws-cdk/aws-events'; +import * as defaults from '@aws-solutions-konstruk/core'; +import * as iam from '@aws-cdk/aws-iam'; +import { Construct } from '@aws-cdk/core'; +import { overrideProps } from '@aws-solutions-konstruk/core'; + +/** + * @summary The properties for the CloudFrontToApiGateway Construct + */ +export interface EventsRuleToLambdaProps { + /** + * Whether to create a new lambda function or use an existing lambda function. + * If set to false, you must provide a lambda function object as `existingObj` + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly lambdaFunctionProps?: lambda.FunctionProps, + /** + * User provided eventRuleProps to override the defaults + * + * @default - None + */ + readonly eventRuleProps: events.RuleProps +} + +export class EventsRuleToLambda extends Construct { + private fn: lambda.Function; + private rule: events.Rule; + + /** + * @summary Constructs a new instance of the EventsRuleToLambda class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {EventsRuleToLambdaProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: EventsRuleToLambdaProps) { + super(scope, id); + + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + + const lambdaFunc: events.IRuleTarget = { + bind: () => ({ + id: '', + arn: this.fn.functionArn + }) + }; + + const defaultEventsRuleProps = defaults.DefaultEventsRuleProps([lambdaFunc]); + const eventsRuleProps = overrideProps(defaultEventsRuleProps, props.eventRuleProps, true); + + this.rule = new events.Rule(this, 'EventsRule', eventsRuleProps); + + this.fn.addPermission("LambdaInvokePermission", { + principal: new iam.ServicePrincipal('events.amazonaws.com'), + sourceArn: this.rule.ruleArn + }); + } + + /** + * @summary Retruns an instance of events.Rule created by the construct. + * @returns {events.Rule} Instance of events.Rule created by the construct + * @since 0.8.0 + * @access public + */ + public eventsRule(): events.Rule { + return this.rule; + } + + /** + * @summary Retruns an instance of lambda.Function created by the construct. + * @returns {lambda.Function} Instance of lambda.Function created by the construct + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/package.json b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/package.json new file mode 100644 index 000000000..80bdf4d2c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/package.json @@ -0,0 +1,79 @@ +{ + "name": "@aws-solutions-konstruk/aws-events-rule-lambda", + "version": "0.8.0", + "description": "CDK Constructs for deploying AWS Events Rule that inveokes AWS Lambda", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.eventsrulelambda", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "eventsrulelambda" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.EventsRuleLambda", + "packageId": "Amazon.Konstruk.AWS.EventsRuleLambda", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-events-rule-lambda", + "module": "aws_solutions_konstruk.aws_events_rule_lambda" + } + } + }, + "dependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-events": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-events": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/__snapshots__/events-rule-lambda.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/__snapshots__/events-rule-lambda.test.js.snap new file mode 100644 index 000000000..b1fe7c586 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/__snapshots__/events-rule-lambda.test.js.snap @@ -0,0 +1,179 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test EventsRuleToLambda default params 1`] = ` +Object { + "Parameters": Object { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": Object { + "Description": "Artifact hash for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": Object { + "Description": "S3 bucket for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": Object { + "Description": "S3 key for asset version \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionLambdaInvokePermissionC135C9F1": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "events.amazonaws.com", + "SourceArn": Object { + "Fn::GetAtt": Array [ + "testeventsrulelambdaEventsRule82B36872", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "testeventsrulelambdaEventsRule82B36872": Object { + "Properties": Object { + "ScheduleExpression": "rate(5 minutes)", + "State": "ENABLED", + "Targets": Array [ + Object { + "Arn": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Id": "Target0", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/events-rule-lambda.test.ts b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/events-rule-lambda.test.ts new file mode 100644 index 000000000..fcae801a4 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/events-rule-lambda.test.ts @@ -0,0 +1,189 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as events from '@aws-cdk/aws-events'; +import { EventsRuleToLambdaProps, EventsRuleToLambda } from '../lib/index'; +import '@aws-cdk/assert/jest'; +import * as cdk from '@aws-cdk/core'; + +function deployNewFunc(stack: cdk.Stack) { + const props: EventsRuleToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, + eventRuleProps: { + schedule: events.Schedule.rate(cdk.Duration.minutes(5)) + } + }; + + return new EventsRuleToLambda(stack, 'test-events-rule-lambda', props); +} + +test('snapshot test EventsRuleToLambda default params', () => { + const stack = new cdk.Stack(); + deployNewFunc(stack); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check lambda function properties for deploy: true', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + Runtime: "nodejs12.x", + Environment: { + Variables: { + AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1" + } + } + }); +}); + +test('check lambda function permission for deploy: true', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::Lambda::Permission', { + Action: "lambda:InvokeFunction", + FunctionName: { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + Principal: "events.amazonaws.com", + SourceArn: { + "Fn::GetAtt": [ + "testeventsrulelambdaEventsRule82B36872", + "Arn" + ] + } + }); +}); + +test('check lambda function role for deploy: true', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "lambda.amazonaws.com" + } + } + ], + Version: "2012-10-17" + }, + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + PolicyName: "LambdaFunctionServiceRolePolicy" + } + ] + }); +}); + +test('check events rule properties for deploy: true', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::Events::Rule', { + ScheduleExpression: "rate(5 minutes)", + State: "ENABLED", + Targets: [ + { + Arn: { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + Id: "Target0" + } + ] + }); +}); + +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: EventsRuleToLambda = deployNewFunc(stack); + + expect(construct.eventsRule()).toBeInstanceOf(events.Rule); + expect(construct.lambdaFunction()).toBeInstanceOf(lambda.Function); +}); + +test('check exception for Missing existingObj from props for deploy = false', () => { + const stack = new cdk.Stack(); + + const props: EventsRuleToLambdaProps = { + deployLambda: false, + eventRuleProps: { + schedule: events.Schedule.rate(cdk.Duration.minutes(5)) + } + }; + + try { + new EventsRuleToLambda(stack, 'test-events-rule-lambda', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); diff --git a/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/integ.events-rule-no-argument.expected.json b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/integ.events-rule-no-argument.expected.json new file mode 100644 index 000000000..f22f4f2ee --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/integ.events-rule-no-argument.expected.json @@ -0,0 +1,175 @@ +{ + "Resources": { + "testeventsrulelambdaEventsRule82B36872": { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "rate(5 minutes)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Id": "Target0" + } + ] + } + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "LambdaFunctionLambdaInvokePermissionC135C9F1": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "testeventsrulelambdaEventsRule82B36872", + "Arn" + ] + } + } + } + }, + "Parameters": { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": { + "Type": "String", + "Description": "S3 bucket for asset \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": { + "Type": "String", + "Description": "S3 key for asset version \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": { + "Type": "String", + "Description": "Artifact hash for asset \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/integ.events-rule-no-argument.ts b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/integ.events-rule-no-argument.ts new file mode 100644 index 000000000..4bd3c48d5 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/integ.events-rule-no-argument.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { EventsRuleToLambda, EventsRuleToLambdaProps } from "../lib"; +import { Duration } from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as events from '@aws-cdk/aws-events'; + +const app = new App(); +const stack = new Stack(app, 'test-events-rule-lambda-stack'); + +const props: EventsRuleToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, + eventRuleProps: { + schedule: events.Schedule.rate(Duration.minutes(5)) + } +}; + +new EventsRuleToLambda(stack, 'test-events-rule-lambda', props); +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/lambda/index.js new file mode 100644 index 000000000..4b3640c1e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-events-rule-lambda/test/lambda/index.js @@ -0,0 +1,10 @@ +console.log('Loading function'); + +exports.handler = async (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); +    return { +      statusCode: 200, +      headers: { 'Content-Type': 'text/plain' }, +      body: `Hello from Project Vesper! You've hit ${event.path}\n` +    }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/README.md b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/README.md new file mode 100644 index 000000000..2062dca0e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/README.md @@ -0,0 +1,82 @@ +# aws-iot-kinesisfirehose-s3 module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-iot-kinesisfirehose-s3/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_iot_kinesisfirehose_s3`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3`| + +This AWS Solutions Konstruk implements an AWS IoT MQTT topic rule to send data to an Amazon Kinesis Data Firehose delivery stream connected to an Amazon S3 bucket. + +Here is a minimal deployable pattern definition: + +``` javascript +const { IotToKinesisFirehoseToS3Props, IotToKinesisFirehoseToS3 } = require('@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3'); + +const props: IotToKinesisFirehoseToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Persistent storage of connected vehicle telematics data", + sql: "SELECT * FROM 'connectedcar/telemetry/#'", + actions: [] + } + } +}; + +new IotToKinesisFirehoseToS3(stack, 'test-iot-firehose-s3', props); + +``` + +## Initializer + +``` text +new IotToKinesisFirehoseToS3(scope: Construct, id: string, props: IotToKinesisFirehoseToS3Props); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`IotToKinesisFirehoseToS3Props`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|iotTopicRuleProps|[`iot.CfnTopicRuleProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iot.CfnTopicRuleProps.html)|User provided CfnTopicRuleProps to override the defaults| +|kinesisFirehoseProps?|[`kinesisfirehose.CfnDeliveryStreamProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kinesisfirehose.CfnDeliveryStreamProps.html)|Optional user provided props to override the default props for Kinesis Firehose Delivery Stream| +|deployBucket?|`boolean`|Whether to create a S3 Bucket or use an existing S3 Bucket| +|existingBucketObj?|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Existing instance of S3 Bucket object| +|bucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|Optional user provided props to override the default props for S3 Bucket| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|kinesisFirehose()|[`kinesisfirehose.CfnDeliveryStream`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kinesisfirehose.CfnDeliveryStream.html)|Retruns an instance of kinesisfirehose.CfnDeliveryStream created by the construct| +|bucket()|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Retruns an instance of s3.Bucket created by the construct| +|iotTopicRule()|[`iot.CfnTopicRule`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iot.CfnTopicRule.html)|Retruns an instance of iot.CfnTopicRule created by the construct| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/architecture.png new file mode 100644 index 000000000..5197c5860 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/lib/index.ts new file mode 100644 index 000000000..66338a9b4 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/lib/index.ts @@ -0,0 +1,146 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as kinesisfirehose from '@aws-cdk/aws-kinesisfirehose'; +import * as iot from '@aws-cdk/aws-iot'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as iam from '@aws-cdk/aws-iam'; +import { Construct } from '@aws-cdk/core'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { overrideProps } from '@aws-solutions-konstruk/core'; +import { KinesisFirehoseToS3 } from '@aws-solutions-konstruk/aws-kinesisfirehose-s3'; + +/** + * @summary The properties for the IotToKinesisFirehoseToS3 Construct + */ +export interface IotToKinesisFirehoseToS3Props { + /** + * User provided CfnTopicRuleProps to override the defaults + * + * @default - Default props are used + */ + readonly iotTopicRuleProps: iot.CfnTopicRuleProps; + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly kinesisFirehoseProps?: kinesisfirehose.CfnDeliveryStreamProps | any; + /** + * Whether to create a S3 Bucket or use an existing S3 Bucket. + * If set to false, you must provide S3 Bucket as `existingBucketObj` + * + * @default - true + */ + readonly deployBucket?: boolean, + /** + * Existing instance of S3 Bucket object. + * If `deployBucket` is set to false only then this property is required + * + * @default - None + */ + readonly existingBucketObj?: s3.Bucket, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly bucketProps?: s3.BucketProps +} + +export class IotToKinesisFirehoseToS3 extends Construct { + private topic: iot.CfnTopicRule; + private firehose: kinesisfirehose.CfnDeliveryStream; + private s3Bucket: s3.Bucket; + + /** + * @summary Constructs a new instance of the IotToKinesisFirehoseToS3 class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {CloudFrontToApiGatewayProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: IotToKinesisFirehoseToS3Props) { + super(scope, id); + + const firehoseToS3 = new KinesisFirehoseToS3(this, 'KinesisFirehoseToS3', { + kinesisFirehoseProps: props.kinesisFirehoseProps, + deployBucket: props.deployBucket, + existingBucketObj: props.existingBucketObj, + bucketProps: props.bucketProps + }); + this.firehose = firehoseToS3.kinesisFirehose(); + this.s3Bucket = firehoseToS3.bucket(); + + // Setup the IAM Role for IoT Actions + const iotActionsRole = new iam.Role(this, 'IotActionsRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + + // Setup the IAM policy for IoT Actions + const iotActionsPolicy = new iam.Policy(this, 'IotActionsPolicy', { + statements: [new iam.PolicyStatement({ + actions: [ + 'firehose:PutRecord' + ], + resources: [this.firehose.attrArn] + }) + ]}); + + // Attach policy to role + iotActionsPolicy.attachToRole(iotActionsRole); + + const defaultIotTopicProps = defaults.DefaultCfnTopicRuleProps([{ + firehose: { + deliveryStreamName: this.firehose.ref, + roleArn: iotActionsRole.roleArn + } + }]); + const iotTopicProps = overrideProps(defaultIotTopicProps, props.iotTopicRuleProps, true); + + // Create the IoT topic rule + this.topic = new iot.CfnTopicRule(this, 'IotTopic', iotTopicProps); + } + + /** + * @summary Retruns an instance of kinesisfirehose.CfnDeliveryStream created by the construct. + * @returns {kinesisfirehose.CfnDeliveryStream} Instance of CfnDeliveryStream created by the construct + * @since 0.8.0 + * @access public + */ + public kinesisFirehose(): kinesisfirehose.CfnDeliveryStream { + return this.firehose as kinesisfirehose.CfnDeliveryStream; + } + + /** + * @summary Retruns an instance of s3.Bucket created by the construct. + * @returns {s3.Bucket} Instance of s3.Bucket created by the construct + * @since 0.8.0 + * @access public + */ + public bucket(): s3.Bucket { + return this.s3Bucket; + } + + /** + * @summary Retruns an instance of iot.CfnTopicRule created by the construct. + * @returns {iot.CfnTopicRule} Instance of CfnTopicRule created by the construct + * @since 0.8.0 + * @access public + */ + public iotTopicRule(): iot.CfnTopicRule { + return this.topic; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/package.json b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/package.json new file mode 100644 index 000000000..16196415d --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/package.json @@ -0,0 +1,82 @@ +{ + "name": "@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3", + "version": "0.8.0", + "description": "CDK Constructs for AWS IoT to AWS Kinesis Firehose to AWS S3 integration.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.iotkinesisfirehoses3", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "iotkinesisfirehoses3" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.IotKinesisfirehoseS3", + "packageId": "Amazon.Konstruk.AWS.IotKinesisfirehoseS3", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-iot-kinesisfirehose-s3", + "module": "aws_solutions_konstruk.aws_iot_kinesisfirehose_s3" + } + } + }, + "dependencies": { + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-kinesisfirehose": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-iot": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-solutions-konstruk/aws-kinesisfirehose-s3": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-kinesisfirehose": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-iot": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-solutions-konstruk/aws-kinesisfirehose-s3": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/test/__snapshots__/test.iot-kinesisfirehose-s3.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/test/__snapshots__/test.iot-kinesisfirehose-s3.test.js.snap new file mode 100644 index 000000000..3a727b2f9 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/test/__snapshots__/test.iot-kinesisfirehose-s3.test.js.snap @@ -0,0 +1,297 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test IotToKinesisFirehoseToS3 default params 1`] = ` +Object { + "Resources": Object { + "testiotfirehoses3IotActionsPolicy1B38E4E3": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "firehose:PutRecord", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "testiotfirehoses3KinesisFirehoseToS3KinesisFirehose68DB2BEE", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "testiotfirehoses3IotActionsPolicy1B38E4E3", + "Roles": Array [ + Object { + "Ref": "testiotfirehoses3IotActionsRole743F8973", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "testiotfirehoses3IotActionsRole743F8973": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "iot.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "testiotfirehoses3IotTopicAC1CA58D": Object { + "Properties": Object { + "TopicRulePayload": Object { + "Actions": Array [ + Object { + "Firehose": Object { + "DeliveryStreamName": Object { + "Ref": "testiotfirehoses3KinesisFirehoseToS3KinesisFirehose68DB2BEE", + }, + "RoleArn": Object { + "Fn::GetAtt": Array [ + "testiotfirehoses3IotActionsRole743F8973", + "Arn", + ], + }, + }, + }, + ], + "Description": "Persistent storage of connected vehicle telematics data", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'connectedcar/telemetry/#'", + }, + }, + "Type": "AWS::IoT::TopicRule", + }, + "testiotfirehoses3KinesisFirehoseToS3KinesisFirehose68DB2BEE": Object { + "Properties": Object { + "ExtendedS3DestinationConfiguration": Object { + "BucketARN": Object { + "Fn::GetAtt": Array [ + "testiotfirehoses3KinesisFirehoseToS3S3Bucket19C97D09", + "Arn", + ], + }, + "BufferingHints": Object { + "IntervalInSeconds": 300, + "SizeInMBs": 5, + }, + "CloudWatchLoggingOptions": Object { + "Enabled": true, + "LogGroupName": Object { + "Ref": "testiotfirehoses3KinesisFirehoseToS3firehoseloggroup4A2E4212", + }, + "LogStreamName": Object { + "Ref": "testiotfirehoses3KinesisFirehoseToS3firehoseloggroupfirehoselogstreamD1B6E670", + }, + }, + "CompressionFormat": "GZIP", + "RoleARN": Object { + "Fn::GetAtt": Array [ + "testiotfirehoses3KinesisFirehoseToS3KinesisFirehoseRole93DE9170", + "Arn", + ], + }, + }, + }, + "Type": "AWS::KinesisFirehose::DeliveryStream", + }, + "testiotfirehoses3KinesisFirehoseToS3KinesisFirehosePolicy5914CC69": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:AbortMultipartUpload", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:PutObject", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "testiotfirehoses3KinesisFirehoseToS3S3Bucket19C97D09", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "testiotfirehoses3KinesisFirehoseToS3S3Bucket19C97D09", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": "logs:PutLogEvents", + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:", + Object { + "Ref": "testiotfirehoses3KinesisFirehoseToS3firehoseloggroup4A2E4212", + }, + ":log-stream:", + Object { + "Ref": "testiotfirehoses3KinesisFirehoseToS3firehoseloggroupfirehoselogstreamD1B6E670", + }, + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "testiotfirehoses3KinesisFirehoseToS3KinesisFirehosePolicy5914CC69", + "Roles": Array [ + Object { + "Ref": "testiotfirehoses3KinesisFirehoseToS3KinesisFirehoseRole93DE9170", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "testiotfirehoses3KinesisFirehoseToS3KinesisFirehoseRole93DE9170": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "firehose.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "testiotfirehoses3KinesisFirehoseToS3S3Bucket19C97D09": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "testiotfirehoses3KinesisFirehoseToS3S3LoggingBucketC786B050", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "testiotfirehoses3KinesisFirehoseToS3S3LoggingBucketC786B050": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "testiotfirehoses3KinesisFirehoseToS3firehoseloggroup4A2E4212": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "testiotfirehoses3KinesisFirehoseToS3firehoseloggroupfirehoselogstreamD1B6E670": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "LogGroupName": Object { + "Ref": "testiotfirehoses3KinesisFirehoseToS3firehoseloggroup4A2E4212", + }, + }, + "Type": "AWS::Logs::LogStream", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..497fb279f --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/test/integ.no-arguments.expected.json @@ -0,0 +1,294 @@ +{ + "Description": "Integration Test for aws-iot-kinesisfirehose-s3", + "Resources": { + "testiotfirehoses3KinesisFirehoseToS3S3LoggingBucketC786B050": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + }, + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "testiotfirehoses3KinesisFirehoseToS3S3Bucket19C97D09": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "testiotfirehoses3KinesisFirehoseToS3S3LoggingBucketC786B050" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "testiotfirehoses3KinesisFirehoseToS3firehoseloggroup4A2E4212": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testiotfirehoses3KinesisFirehoseToS3firehoseloggroupfirehoselogstreamD1B6E670": { + "Type": "AWS::Logs::LogStream", + "Properties": { + "LogGroupName": { + "Ref": "testiotfirehoses3KinesisFirehoseToS3firehoseloggroup4A2E4212" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testiotfirehoses3KinesisFirehoseToS3KinesisFirehoseRole93DE9170": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "firehose.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testiotfirehoses3KinesisFirehoseToS3KinesisFirehosePolicy5914CC69": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:AbortMultipartUpload", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:PutObject" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testiotfirehoses3KinesisFirehoseToS3S3Bucket19C97D09", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testiotfirehoses3KinesisFirehoseToS3S3Bucket19C97D09", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "logs:PutLogEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "testiotfirehoses3KinesisFirehoseToS3firehoseloggroup4A2E4212" + }, + ":log-stream:", + { + "Ref": "testiotfirehoses3KinesisFirehoseToS3firehoseloggroupfirehoselogstreamD1B6E670" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testiotfirehoses3KinesisFirehoseToS3KinesisFirehosePolicy5914CC69", + "Roles": [ + { + "Ref": "testiotfirehoses3KinesisFirehoseToS3KinesisFirehoseRole93DE9170" + } + ] + } + }, + "testiotfirehoses3KinesisFirehoseToS3KinesisFirehose68DB2BEE": { + "Type": "AWS::KinesisFirehose::DeliveryStream", + "Properties": { + "ExtendedS3DestinationConfiguration": { + "BucketARN": { + "Fn::GetAtt": [ + "testiotfirehoses3KinesisFirehoseToS3S3Bucket19C97D09", + "Arn" + ] + }, + "BufferingHints": { + "IntervalInSeconds": 300, + "SizeInMBs": 5 + }, + "CloudWatchLoggingOptions": { + "Enabled": true, + "LogGroupName": { + "Ref": "testiotfirehoses3KinesisFirehoseToS3firehoseloggroup4A2E4212" + }, + "LogStreamName": { + "Ref": "testiotfirehoses3KinesisFirehoseToS3firehoseloggroupfirehoselogstreamD1B6E670" + } + }, + "CompressionFormat": "GZIP", + "RoleARN": { + "Fn::GetAtt": [ + "testiotfirehoses3KinesisFirehoseToS3KinesisFirehoseRole93DE9170", + "Arn" + ] + } + } + } + }, + "testiotfirehoses3IotActionsRole743F8973": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testiotfirehoses3IotActionsPolicy1B38E4E3": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "firehose:PutRecord", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testiotfirehoses3KinesisFirehoseToS3KinesisFirehose68DB2BEE", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testiotfirehoses3IotActionsPolicy1B38E4E3", + "Roles": [ + { + "Ref": "testiotfirehoses3IotActionsRole743F8973" + } + ] + } + }, + "testiotfirehoses3IotTopicAC1CA58D": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Firehose": { + "DeliveryStreamName": { + "Ref": "testiotfirehoses3KinesisFirehoseToS3KinesisFirehose68DB2BEE" + }, + "RoleArn": { + "Fn::GetAtt": [ + "testiotfirehoses3IotActionsRole743F8973", + "Arn" + ] + } + } + } + ], + "Description": "Persistent storage of connected vehicle telematics data", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'connectedcar/telemetry/#'" + } + } + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/test/integ.no-arguments.ts new file mode 100644 index 000000000..0e35ba314 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/test/integ.no-arguments.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { IotToKinesisFirehoseToS3, IotToKinesisFirehoseToS3Props } from "../lib"; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-iot-firehose-s3-stack'); +stack.templateOptions.description = 'Integration Test for aws-iot-kinesisfirehose-s3'; + +// Definitions +const props: IotToKinesisFirehoseToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Persistent storage of connected vehicle telematics data", + sql: "SELECT * FROM 'connectedcar/telemetry/#'", + actions: [] + } + } +}; + +new IotToKinesisFirehoseToS3(stack, 'test-iot-firehose-s3', props); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/test/test.iot-kinesisfirehose-s3.test.ts b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/test/test.iot-kinesisfirehose-s3.test.ts new file mode 100644 index 000000000..a78b694fb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-kinesisfirehose-s3/test/test.iot-kinesisfirehose-s3.test.ts @@ -0,0 +1,126 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { IotToKinesisFirehoseToS3, IotToKinesisFirehoseToS3Props } from "../lib"; +import * as cdk from "@aws-cdk/core"; +import * as iot from '@aws-cdk/aws-iot'; +import '@aws-cdk/assert/jest'; + +function deploy(stack: cdk.Stack) { + const props: IotToKinesisFirehoseToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Persistent storage of connected vehicle telematics data", + sql: "SELECT * FROM 'connectedcar/telemetry/#'", + actions: [] + } + } + }; + + return new IotToKinesisFirehoseToS3(stack, 'test-iot-firehose-s3', props); +} + +test('snapshot test IotToKinesisFirehoseToS3 default params', () => { + const stack = new cdk.Stack(); + deploy(stack); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check iot topic rule properties', () => { + const stack = new cdk.Stack(); + + deploy(stack); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Firehose: { + DeliveryStreamName: { + Ref: "testiotfirehoses3KinesisFirehoseToS3KinesisFirehose68DB2BEE" + }, + RoleArn: { + "Fn::GetAtt": [ + "testiotfirehoses3IotActionsRole743F8973", + "Arn" + ] + } + } + } + ], + Description: "Persistent storage of connected vehicle telematics data", + RuleDisabled: false, + Sql: "SELECT * FROM 'connectedcar/telemetry/#'" + } + }); +}); + +test('check firehose and s3 overrides', () => { + const stack = new cdk.Stack(); + + const props: IotToKinesisFirehoseToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Persistent storage of connected vehicle telematics data", + sql: "SELECT * FROM 'connectedcar/telemetry/#'", + actions: [] + } + }, + kinesisFirehoseProps: { + extendedS3DestinationConfiguration: { + bufferingHints: { + intervalInSeconds: 600, + sizeInMBs: 55 + }, + } + }, + bucketProps: { + blockPublicAccess: { + blockPublicAcls: false, + blockPublicPolicy: true, + ignorePublicAcls: false, + restrictPublicBuckets: true + } + } + }; + new IotToKinesisFirehoseToS3(stack, 'test-iot-firehose-s3', props); + + expect(stack).toHaveResource("AWS::S3::Bucket", { + PublicAccessBlockConfiguration: { + BlockPublicAcls: false, + BlockPublicPolicy: true, + IgnorePublicAcls: false, + RestrictPublicBuckets: true + }, + }); + + expect(stack).toHaveResourceLike("AWS::KinesisFirehose::DeliveryStream", { + ExtendedS3DestinationConfiguration: { + BufferingHints: { + IntervalInSeconds: 600, + SizeInMBs: 55 + } + }}); +}); +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: IotToKinesisFirehoseToS3 = deploy(stack); + + expect(construct.iotTopicRule()).toBeInstanceOf(iot.CfnTopicRule); + expect(construct.kinesisFirehose()).toBeDefined(); + expect(construct.bucket()).toBeDefined(); +}); diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/README.md b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/README.md new file mode 100644 index 000000000..57985051b --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/README.md @@ -0,0 +1,88 @@ +# aws-iot-lambda-dynamodb module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-iot-lambda-dynamodb/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_iot_lambda_dynamodb`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-iot-lambda-dynamodb`| + +This AWS Solutions Konstruk implements an AWS IoT topic rule, an AWS Lambda function and Amazon DynamoDB table with the least privileged permissions. + +Here is a minimal deployable pattern definition: + +``` javascript +const { IotToLambdaToDynamoDBProps, IotToLambdaToDynamoDB } = require('@aws-solutions-konstruk/aws-iot-lambda-dynamodb'); + +const props: IotToLambdaToDynamoDBProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } +}; + +new IotToLambdaToDynamoDB(stack, 'test-iot-lambda-dynamodb-stack', props); + +``` + +## Initializer + +``` text +new IotToLambdaToDynamoDB(scope: Construct, id: string, props: IotToLambdaToDynamoDBProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`IotToLambdaToDynamoDBProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new lambda function or use an existing lambda function| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Existing instance of Lambda Function object| +|lambdaFunctionProps|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user provided props to override the default props for lambda.Function| +|iotTopicRuleProps|[`iot.CfnTopicRuleProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iot.CfnTopicRuleProps.html)|User provided props to override the default props| +|dynamoTableProps?|[`dynamodb.TableProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.TableProps.html)|Optional user provided props to override the default props for DynamoDB Table| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|iotTopicRule()|[`iot.CfnTopicRule`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iot.CfnTopicRule.html)|Retruns an instance of iot.CfnTopicRule created by the construct| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Retruns an instance of lambda.Function created by the construct| +|dynamoTable()|[`dynamodb.Table`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.Table.html)|Retruns an instance of dynamodb.Table created by the construct| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/architecture.png new file mode 100644 index 000000000..f1f3a0f05 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/lib/index.ts new file mode 100644 index 000000000..c83d31540 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/lib/index.ts @@ -0,0 +1,119 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as lambda from '@aws-cdk/aws-lambda'; +import * as iot from '@aws-cdk/aws-iot'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import { IotToLambda } from '@aws-solutions-konstruk/aws-iot-lambda'; +import { LambdaToDynamoDB } from '@aws-solutions-konstruk/aws-lambda-dynamodb'; +import { Construct } from '@aws-cdk/core'; + +/** + * @summary The properties for the IotToLambdaToDynamoDB class. + */ +export interface IotToLambdaToDynamoDBProps { + /** + * Whether to create a new lambda function or use an existing lambda function. + * If set to false, you must provide a lambda function object as `existingObj` + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly lambdaFunctionProps?: lambda.FunctionProps, + /** + * User provided props to override the default props + * + * @default - Default props are used + */ + readonly iotTopicRuleProps: iot.CfnTopicRuleProps, + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly dynamoTableProps?: dynamodb.TableProps +} + +export class IotToLambdaToDynamoDB extends Construct { + private topic: iot.CfnTopicRule; + private fn: lambda.Function; + private table: dynamodb.Table; + + /** + * @summary Constructs a new instance of the IotToLambdaToDynamoDB class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {IotToLambdaToDynamoDBProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: IotToLambdaToDynamoDBProps) { + super(scope, id); + + // Setup the IotToLambda + const iotToLambda = new IotToLambda(this, 'IotToLambda', props); + this.topic = iotToLambda.iotTopicRule(); + this.fn = iotToLambda.lambdaFunction(); + + // Setup the LambdaToDynamoDB + const lambdaToDynamoDB = new LambdaToDynamoDB(this, 'LambdaToDynamoDB', { + deployLambda: false, + existingLambdaObj: this.fn, + dynamoTableProps: props.dynamoTableProps + }); + this.table = lambdaToDynamoDB.dynamoTable(); + } + + /** + * @summary Retruns an instance of iot.CfnTopicRule created by the construct. + * @returns {iot.CfnTopicRule} Instance of CfnTopicRule created by the construct + * @since 0.8.0 + * @access public + */ + public iotTopicRule(): iot.CfnTopicRule { + return this.topic; + } + + /** + * @summary Retruns an instance of lambda.Function created by the construct. + * @returns {lambda.Function} Instance of lambda.Function created by the construct + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } + + /** + * @summary Retruns an instance of dynamodb.Table created by the construct. + * @returns {dynamodb.Table} Instance of dynamodb.Table created by the construct + * @since 0.8.0 + * @access public + */ + public dynamoTable(): dynamodb.Table { + return this.table; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/package.json b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/package.json new file mode 100644 index 000000000..ba1bbc812 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/package.json @@ -0,0 +1,83 @@ +{ + "name": "@aws-solutions-konstruk/aws-iot-lambda-dynamodb", + "version": "0.8.0", + "description": "CDK Constructs for AWS IoT to AWS Lambda to AWS DyanmoDB integration.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.iotlambdadynamodb", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "iotlambdadynamodb" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.IotLambdaDynamodb", + "packageId": "Amazon.Konstruk.AWS.IotLambdaDynamodb", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-iot-lambda-dynamodb", + "module": "aws_solutions_konstruk.aws_iot_lambda_dynamodb" + } + } + }, + "dependencies": { + "@aws-cdk/aws-dynamodb": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-iot": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-solutions-konstruk/aws-iot-lambda": "~0.8.0", + "@aws-solutions-konstruk/aws-lambda-dynamodb": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-dynamodb": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-iot": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-solutions-konstruk/aws-iot-lambda": "~0.8.0", + "@aws-solutions-konstruk/aws-lambda-dynamodb": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/__snapshots__/iot-lambda-dynamodb.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/__snapshots__/iot-lambda-dynamodb.test.js.snap new file mode 100644 index 000000000..df427159f --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/__snapshots__/iot-lambda-dynamodb.test.js.snap @@ -0,0 +1,252 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test IotToLambdaToDynamoDB default params 1`] = ` +Object { + "Parameters": Object { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": Object { + "Description": "Artifact hash for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": Object { + "Description": "S3 bucket for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": Object { + "Description": "S3 key for asset version \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + }, + "Resources": Object { + "testiotlambdadynamodbstackIotToLambdaIotTopic74F5E3BB": Object { + "Properties": Object { + "TopicRulePayload": Object { + "Actions": Array [ + Object { + "Lambda": Object { + "FunctionArn": Object { + "Fn::GetAtt": Array [ + "testiotlambdadynamodbstackLambdaFunctionF1BF28BF", + "Arn", + ], + }, + }, + }, + ], + "Description": "Processing of DTC messages from the AWS Connected Vehicle Solution.", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'connectedcar/dtc/#'", + }, + }, + "Type": "AWS::IoT::TopicRule", + }, + "testiotlambdadynamodbstackLambdaFunctionF1BF28BF": Object { + "DependsOn": Array [ + "testiotlambdadynamodbstackLambdaFunctionServiceRoleDefaultPolicyAE674B0C", + "testiotlambdadynamodbstackLambdaFunctionServiceRoleA709DBA2", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": Object { + "Ref": "testiotlambdadynamodbstackLambdaToDynamoDBDynamoTableE17E5733", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "testiotlambdadynamodbstackLambdaFunctionServiceRoleA709DBA2", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "testiotlambdadynamodbstackLambdaFunctionLambdaInvokePermissionC33462B4": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "testiotlambdadynamodbstackLambdaFunctionF1BF28BF", + "Arn", + ], + }, + "Principal": "iot.amazonaws.com", + "SourceArn": Object { + "Fn::GetAtt": Array [ + "testiotlambdadynamodbstackIotToLambdaIotTopic74F5E3BB", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "testiotlambdadynamodbstackLambdaFunctionServiceRoleA709DBA2": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "testiotlambdadynamodbstackLambdaFunctionServiceRoleDefaultPolicyAE674B0C": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "testiotlambdadynamodbstackLambdaToDynamoDBDynamoTableE17E5733", + "Arn", + ], + }, + Object { + "Ref": "AWS::NoValue", + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "testiotlambdadynamodbstackLambdaFunctionServiceRoleDefaultPolicyAE674B0C", + "Roles": Array [ + Object { + "Ref": "testiotlambdadynamodbstackLambdaFunctionServiceRoleA709DBA2", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "testiotlambdadynamodbstackLambdaToDynamoDBDynamoTableE17E5733": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "AttributeDefinitions": Array [ + Object { + "AttributeName": "id", + "AttributeType": "S", + }, + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": Array [ + Object { + "AttributeName": "id", + "KeyType": "HASH", + }, + ], + "SSESpecification": Object { + "SSEEnabled": true, + }, + }, + "Type": "AWS::DynamoDB::Table", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/integ.iot-lambda-dynamodb.expected.json b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/integ.iot-lambda-dynamodb.expected.json new file mode 100644 index 000000000..96f1fadee --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/integ.iot-lambda-dynamodb.expected.json @@ -0,0 +1,248 @@ +{ + "Resources": { + "testiotlambdadynamodbstackIotToLambdaIotTopic74F5E3BB": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Lambda": { + "FunctionArn": { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaFunctionF1BF28BF", + "Arn" + ] + } + } + } + ], + "Description": "Processing of DTC messages from the AWS Connected Vehicle Solution.", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'connectedcar/dtc/#'" + } + } + }, + "testiotlambdadynamodbstackLambdaFunctionServiceRoleA709DBA2": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "testiotlambdadynamodbstackLambdaFunctionServiceRoleDefaultPolicyAE674B0C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaToDynamoDBDynamoTableE17E5733", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testiotlambdadynamodbstackLambdaFunctionServiceRoleDefaultPolicyAE674B0C", + "Roles": [ + { + "Ref": "testiotlambdadynamodbstackLambdaFunctionServiceRoleA709DBA2" + } + ] + } + }, + "testiotlambdadynamodbstackLambdaFunctionF1BF28BF": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaFunctionServiceRoleA709DBA2", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": { + "Ref": "testiotlambdadynamodbstackLambdaToDynamoDBDynamoTableE17E5733" + } + } + } + }, + "DependsOn": [ + "testiotlambdadynamodbstackLambdaFunctionServiceRoleDefaultPolicyAE674B0C", + "testiotlambdadynamodbstackLambdaFunctionServiceRoleA709DBA2" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "testiotlambdadynamodbstackLambdaFunctionLambdaInvokePermissionC33462B4": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaFunctionF1BF28BF", + "Arn" + ] + }, + "Principal": "iot.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackIotToLambdaIotTopic74F5E3BB", + "Arn" + ] + } + } + }, + "testiotlambdadynamodbstackLambdaToDynamoDBDynamoTableE17E5733": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + } + }, + "Parameters": { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": { + "Type": "String", + "Description": "S3 bucket for asset \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": { + "Type": "String", + "Description": "S3 key for asset version \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": { + "Type": "String", + "Description": "Artifact hash for asset \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/integ.iot-lambda-dynamodb.ts b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/integ.iot-lambda-dynamodb.ts new file mode 100644 index 000000000..203dc16eb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/integ.iot-lambda-dynamodb.ts @@ -0,0 +1,41 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { IotToLambdaToDynamoDB, IotToLambdaToDynamoDBProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; + +const app = new App(); +const stack = new Stack(app, 'test-iot-lambda-dynamodb-stack'); + +const props: IotToLambdaToDynamoDBProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } +}; + +new IotToLambdaToDynamoDB(stack, 'test-iot-lambda-dynamodb-stack', props); + +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/iot-lambda-dynamodb.test.ts b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/iot-lambda-dynamodb.test.ts new file mode 100644 index 000000000..5cfad31cd --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/iot-lambda-dynamodb.test.ts @@ -0,0 +1,273 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { IotToLambdaToDynamoDB, IotToLambdaToDynamoDBProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from "@aws-cdk/core"; +import '@aws-cdk/assert/jest'; + +function deployStack(stack: cdk.Stack) { + const props: IotToLambdaToDynamoDBProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } + }; + + return new IotToLambdaToDynamoDB(stack, 'test-iot-lambda-dynamodb-stack', props); +} + +test('snapshot test IotToLambdaToDynamoDB default params', () => { + const stack = new cdk.Stack(); + deployStack(stack); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check lambda function properties', () => { + const stack = new cdk.Stack(); + + deployStack(stack); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaFunctionServiceRoleA709DBA2", + "Arn" + ] + }, + Runtime: "nodejs10.x", + Environment: { + Variables: { + AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1", + DDB_TABLE_NAME: { + Ref: "testiotlambdadynamodbstackLambdaToDynamoDBDynamoTableE17E5733" + } + } + } + }); +}); + +test('check lambda function permission', () => { + const stack = new cdk.Stack(); + + deployStack(stack); + + expect(stack).toHaveResource('AWS::Lambda::Permission', { + Action: "lambda:InvokeFunction", + FunctionName: { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaFunctionF1BF28BF", + "Arn" + ] + }, + Principal: "iot.amazonaws.com", + SourceArn: { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackIotToLambdaIotTopic74F5E3BB", + "Arn" + ] + } + }); +}); + +test('check iot lambda function role', () => { + const stack = new cdk.Stack(); + + deployStack(stack); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "lambda.amazonaws.com" + } + } + ], + Version: "2012-10-17" + }, + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + PolicyName: "LambdaFunctionServiceRolePolicy" + } + ] + }); +}); + +test('check iot topic rule properties', () => { + const stack = new cdk.Stack(); + + deployStack(stack); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Lambda: { + FunctionArn: { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaFunctionF1BF28BF", + "Arn" + ] + } + } + } + ], + Description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + RuleDisabled: false, + Sql: "SELECT * FROM 'connectedcar/dtc/#'" + } + }); + +}); + +test('check dynamo table properties', () => { + const stack = new cdk.Stack(); + + deployStack(stack); + + expect(stack).toHaveResource('AWS::DynamoDB::Table', { + KeySchema: [ + { + AttributeName: "id", + KeyType: "HASH" + } + ], + AttributeDefinitions: [ + { + AttributeName: "id", + AttributeType: "S" + } + ], + BillingMode: "PAY_PER_REQUEST", + SSESpecification: { + SSEEnabled: true + } + }); +}); + +test('check lambda function policy ', () => { + const stack = new cdk.Stack(); + + deployStack(stack); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem" + ], + Effect: "Allow", + Resource: [ + { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaToDynamoDBDynamoTableE17E5733", + "Arn" + ] + }, + { + Ref: "AWS::NoValue" + } + ] + } + ], + Version: "2012-10-17" + } + }); + +}); + +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: IotToLambdaToDynamoDB = deployStack(stack); + + expect(construct.lambdaFunction()).toBeDefined(); + expect(construct.dynamoTable()).toBeDefined(); + expect(construct.iotTopicRule()).toBeDefined(); +}); + +test('check exception for Missing existingObj from props for deploy = false', () => { + const stack = new cdk.Stack(); + + const props: IotToLambdaToDynamoDBProps = { + deployLambda: false, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } + }; + + try { + new IotToLambdaToDynamoDB(stack, 'test-iot-lambda-integration', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/lambda/index.js new file mode 100644 index 000000000..4b3640c1e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda-dynamodb/test/lambda/index.js @@ -0,0 +1,10 @@ +console.log('Loading function'); + +exports.handler = async (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); +    return { +      statusCode: 200, +      headers: { 'Content-Type': 'text/plain' }, +      body: `Hello from Project Vesper! You've hit ${event.path}\n` +    }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/README.md b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/README.md new file mode 100644 index 000000000..9bdbbf5b3 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/README.md @@ -0,0 +1,85 @@ +# aws-iot-lambda module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-iot-lambda/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_iot_lambda`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-iot-lambda`| + +This AWS Solutions Konstruk implements an AWS IoT MQTT topic rule and an AWS Lambda function pattern. + +Here is a minimal deployable pattern definition: + +``` javascript +const { IotToLambdaProps, IotToLambda } = require('@aws-solutions-konstruk/aws-iot-lambda'); + +const props: IotToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } +}; + +new IotToLambda(stack, 'test-iot-lambda-integration', props); +``` + +## Initializer + +``` text +new IotToLambda(scope: Construct, id: string, props: IotToLambdaProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`IotToLambdaProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new lambda function or use an existing lambda function| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Existing instance of Lambda Function object| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user provided props to override the default props for lambda.Function| +|iotTopicRuleProps?|[`iot.CfnTopicRuleProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iot.CfnTopicRuleProps.html)|User provided CfnTopicRuleProps to override the defaults| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|iotTopicRule()|[`iot.CfnTopicRule`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iot.CfnTopicRule.html)|Retruns an instance of iot.CfnTopicRule created by the construct| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Retruns an instance of lambda.Function created by the construct| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/architecture.png new file mode 100644 index 000000000..2b9c6c7ad Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/lib/index.ts new file mode 100644 index 000000000..b9a61de43 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/lib/index.ts @@ -0,0 +1,111 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as lambda from '@aws-cdk/aws-lambda'; +import * as iot from '@aws-cdk/aws-iot'; +import * as iam from '@aws-cdk/aws-iam'; +import { Construct } from '@aws-cdk/core'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { overrideProps } from '@aws-solutions-konstruk/core'; + +/** + * @summary The properties for the IotToLambda class. + */ +export interface IotToLambdaProps { + /** + * Whether to create a new lambda function or use an existing lambda function. + * If set to false, you must provide a lambda function object as `existingObj` + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly lambdaFunctionProps?: lambda.FunctionProps, + /** + * User provided CfnTopicRuleProps to override the defaults + * + * @default - None + */ + readonly iotTopicRuleProps: iot.CfnTopicRuleProps +} + +export class IotToLambda extends Construct { + private fn: lambda.Function; + private topic: iot.CfnTopicRule; + + /** + * @summary Constructs a new instance of the IotToLambda class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {IotToLambdaProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: IotToLambdaProps) { + super(scope, id); + + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + + const defaultIotTopicProps = defaults.DefaultCfnTopicRuleProps([{ + lambda: { + functionArn: this.fn.functionArn + } + }]); + const iotTopicProps = overrideProps(defaultIotTopicProps, props.iotTopicRuleProps, true); + + // Create the IoT topic rule + this.topic = new iot.CfnTopicRule(this, 'IotTopic', iotTopicProps); + + this.fn.addPermission("LambdaInvokePermission", { + principal: new iam.ServicePrincipal('iot.amazonaws.com'), + sourceArn: this.topic.attrArn + }); + } + + /** + * @summary Retruns an instance of iot.CfnTopicRule created by the construct. + * @returns {iot.CfnTopicRule} Instance of CfnTopicRule created by the construct + * @since 0.8.0 + * @access public + */ + public iotTopicRule(): iot.CfnTopicRule { + return this.topic; + } + + /** + * @summary Retruns an instance of lambda.Function created by the construct. + * @returns {lambda.Function} Instance of lambda.Function created by the construct + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } + +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/package.json b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/package.json new file mode 100644 index 000000000..a34c9bb12 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/package.json @@ -0,0 +1,79 @@ +{ + "name": "@aws-solutions-konstruk/aws-iot-lambda", + "version": "0.8.0", + "description": "CDK Constructs for AWS IoT to AWS Lambda integration", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-iot-lambda" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.iotlambda", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "iotlambda" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.IotLambda", + "packageId": "Amazon.Konstruk.AWS.IotLambda", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-iot-lambda", + "module": "aws_solutions_konstruk.aws_iot_lambda" + } + } + }, + "dependencies": { + "@aws-cdk/aws-iot": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-iot": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-cdk/aws-iam": "~1.25.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/__snapshots__/iot-lambda.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/__snapshots__/iot-lambda.test.js.snap new file mode 100644 index 000000000..88383f6cf --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/__snapshots__/iot-lambda.test.js.snap @@ -0,0 +1,183 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test IotToLambda default params 1`] = ` +Object { + "Parameters": Object { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": Object { + "Description": "Artifact hash for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": Object { + "Description": "S3 bucket for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": Object { + "Description": "S3 key for asset version \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionLambdaInvokePermissionC135C9F1": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "iot.amazonaws.com", + "SourceArn": Object { + "Fn::GetAtt": Array [ + "testiotlambdaintegrationIotTopic18B6A735", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "testiotlambdaintegrationIotTopic18B6A735": Object { + "Properties": Object { + "TopicRulePayload": Object { + "Actions": Array [ + Object { + "Lambda": Object { + "FunctionArn": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + }, + }, + ], + "Description": "Processing of DTC messages from the AWS Connected Vehicle Solution.", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'connectedcar/dtc/#'", + }, + }, + "Type": "AWS::IoT::TopicRule", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/integ.iot-lambda-new-func.expected.json b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/integ.iot-lambda-new-func.expected.json new file mode 100644 index 000000000..046adef40 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/integ.iot-lambda-new-func.expected.json @@ -0,0 +1,179 @@ +{ + "Resources": { + "testiotlambdaintegrationIotTopic18B6A735": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Lambda": { + "FunctionArn": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + } + } + } + ], + "Description": "Processing of DTC messages from the AWS Connected Vehicle Solution.", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'connectedcar/dtc/#'" + } + } + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "LambdaFunctionLambdaInvokePermissionC135C9F1": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "iot.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "testiotlambdaintegrationIotTopic18B6A735", + "Arn" + ] + } + } + } + }, + "Parameters": { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": { + "Type": "String", + "Description": "S3 bucket for asset \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": { + "Type": "String", + "Description": "S3 key for asset version \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": { + "Type": "String", + "Description": "Artifact hash for asset \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/integ.iot-lambda-new-func.ts b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/integ.iot-lambda-new-func.ts new file mode 100644 index 000000000..49b7cedd8 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/integ.iot-lambda-new-func.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { IotToLambda, IotToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; + +const app = new App(); +const stack = new Stack(app, 'test-iot-lambda-stack'); + +const props: IotToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } +}; + +new IotToLambda(stack, 'test-iot-lambda-integration', props); +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/integ.iot-lambda-use-existing-func.expected.json b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/integ.iot-lambda-use-existing-func.expected.json new file mode 100644 index 000000000..d91fca779 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/integ.iot-lambda-use-existing-func.expected.json @@ -0,0 +1,179 @@ +{ + "Resources": { + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "LambdaFunctionLambdaInvokePermissionC135C9F1": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "iot.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "testiotlambdaintegrationIotTopic18B6A735", + "Arn" + ] + } + } + }, + "testiotlambdaintegrationIotTopic18B6A735": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Lambda": { + "FunctionArn": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + } + } + } + ], + "Description": "Processing of DTC messages from the AWS Connected Vehicle Solution.", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'connectedcar/dtc/#'" + } + } + } + }, + "Parameters": { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": { + "Type": "String", + "Description": "S3 bucket for asset \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": { + "Type": "String", + "Description": "S3 key for asset version \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": { + "Type": "String", + "Description": "Artifact hash for asset \"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/integ.iot-lambda-use-existing-func.ts b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/integ.iot-lambda-use-existing-func.ts new file mode 100644 index 000000000..c6f74df93 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/integ.iot-lambda-use-existing-func.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { IotToLambda, IotToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as defaults from '@aws-solutions-konstruk/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-iot-lambda-stack'); + +const lambdaFunctionProps = { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) +}; + +const func = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + +// Definitions +const props: IotToLambdaProps = { + deployLambda: false, + existingLambdaObj: func, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } +}; + +new IotToLambda(stack, 'test-iot-lambda-integration', props); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/iot-lambda.test.ts b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/iot-lambda.test.ts new file mode 100644 index 000000000..c975f3658 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/iot-lambda.test.ts @@ -0,0 +1,319 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { IotToLambda, IotToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as iot from '@aws-cdk/aws-iot'; +import * as cdk from "@aws-cdk/core"; +import '@aws-cdk/assert/jest'; + +function deployNewFunc(stack: cdk.Stack) { + const props: IotToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } + }; + + return new IotToLambda(stack, 'test-iot-lambda-integration', props); +} + +function useExistingFunc(stack: cdk.Stack) { + + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.PYTHON_3_6, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + const props: IotToLambdaProps = { + deployLambda: false, + existingLambdaObj: new lambda.Function(stack, 'MyExistingFunction', lambdaFunctionProps), + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } + }; + + return new IotToLambda(stack, 'test-iot-lambda-integration', props); +} + +test('snapshot test IotToLambda default params', () => { + const stack = new cdk.Stack(); + deployNewFunc(stack); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check lambda function properties for deploy: true', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + Runtime: "nodejs10.x" + }); +}); + +test('check lambda function permission for deploy: true', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::Lambda::Permission', { + Action: "lambda:InvokeFunction", + FunctionName: { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + Principal: "iot.amazonaws.com", + SourceArn: { + "Fn::GetAtt": [ + "testiotlambdaintegrationIotTopic18B6A735", + "Arn" + ] + } + }); +}); + +test('check iot lambda function role for deploy: true', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "lambda.amazonaws.com" + } + } + ], + Version: "2012-10-17" + }, + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + PolicyName: "LambdaFunctionServiceRolePolicy" + } + ] + }); +}); + +test('check iot topic rule properties for deploy: true', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Lambda: { + FunctionArn: { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + } + } + } + ], + Description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + RuleDisabled: false, + Sql: "SELECT * FROM 'connectedcar/dtc/#'" + } + }); +}); + +test('check lambda function properties for deploy: false', () => { + const stack = new cdk.Stack(); + + useExistingFunc(stack); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "MyExistingFunctionServiceRoleF9E14BFD", + "Arn" + ] + }, + Runtime: "python3.6" + }); +}); + +test('check lambda function permissions for deploy: false', () => { + const stack = new cdk.Stack(); + + useExistingFunc(stack); + + expect(stack).toHaveResource('AWS::Lambda::Permission', { + Action: "lambda:InvokeFunction", + FunctionName: { + "Fn::GetAtt": [ + "MyExistingFunction4D772515", + "Arn" + ] + }, + Principal: "iot.amazonaws.com", + SourceArn: { + "Fn::GetAtt": [ + "testiotlambdaintegrationIotTopic18B6A735", + "Arn" + ] + } + }); +}); + +test('check iot lambda function role for deploy: false', () => { + const stack = new cdk.Stack(); + + useExistingFunc(stack); + + expect(stack).toHaveResource('AWS::IAM::Role', { + 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" + ] + ] + } + ] + }); +}); + +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: IotToLambda = deployNewFunc(stack); + + expect(construct.iotTopicRule()).toBeInstanceOf(iot.CfnTopicRule); + expect(construct.lambdaFunction()).toBeInstanceOf(lambda.Function); +}); + +test('check exception for Missing existingObj from props for deploy = false', () => { + const stack = new cdk.Stack(); + + const props: IotToLambdaProps = { + deployLambda: false, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } + }; + + try { + new IotToLambda(stack, 'test-iot-lambda-integration', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); + +test('check deploy = true and no prop', () => { + const stack = new cdk.Stack(); + + const props: IotToLambdaProps = { + deployLambda: true, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } + }; + + try { + new IotToLambda(stack, 'test-iot-lambda-integration', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/lambda/index.js new file mode 100644 index 000000000..4b3640c1e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-iot-lambda/test/lambda/index.js @@ -0,0 +1,10 @@ +console.log('Loading function'); + +exports.handler = async (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); +    return { +      statusCode: 200, +      headers: { 'Content-Type': 'text/plain' }, +      body: `Hello from Project Vesper! You've hit ${event.path}\n` +    }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/README.md b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/README.md new file mode 100644 index 000000000..c5ab3a024 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/README.md @@ -0,0 +1,102 @@ +# aws-kinesisfirehose-s3-and-kinesisanalytics module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-kinesisfirehose-s3-and-kinesisanalytics/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_kinesisfirehose_s3_and_kinesisanalytics`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics`| + +This AWS Solutions Konstruk implements an Amazon Kinesis Firehose delivery stream connected to: +1. An Amazon S3 bucket, and +1. An Amazon Kinesis Analytics application. + +Here is a minimal deployable pattern definition: + +``` javascript +const { KinesisFirehoseToAnalyticsAndS3 } = require('@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics'); + +new KinesisFirehoseToAnalyticsAndS3(stack, 'FirehoseToS3AndAnalyticsPattern', { + kinesisAnalyticsProps: { + inputs: [{ + inputSchema: { + recordColumns: [{ + name: 'ticker_symbol', + sqlType: 'VARCHAR(4)', + mapping: '$.ticker_symbol' + }, { + name: 'sector', + sqlType: 'VARCHAR(16)', + mapping: '$.sector' + }, { + name: 'change', + sqlType: 'REAL', + mapping: '$.change' + }, { + name: 'price', + sqlType: 'REAL', + mapping: '$.price' + }], + recordFormat: { + recordFormatType: 'JSON' + }, + recordEncoding: 'UTF-8' + }, + namePrefix: 'SOURCE_SQL_STREAM' + }] + } +}); + +``` + +## Initializer + +``` text +new KinesisFirehoseToAnalyticsAndS3(scope: Construct, id: string, props: KinesisFirehoseToAnalyticsAndS3Props); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`KinesisFirehoseToAnalyticsAndS3Props`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|kinesisFirehoseProps?|[`kinesisFirehose.CfnDeliveryStreamProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kinesisfirehose.CfnDeliveryStreamProps.html)|Optional user-provided props to override the default props for the Kinesis Firehose delivery stream.| +|kinesisAnalyticsProps?|[`kinesisAnalytics.CfnApplicationProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kinesisanalytics.CfnApplicationProps.html)|Optional user-provided props to override the default props for the Kinesis Analytics application.| +|deployBucket?|`boolean`|Whether to create a S3 Bucket or use an existing S3 Bucket| +|existingBucketObj?|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Existing instance of S3 Bucket object| +|bucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|Optional user provided props to override the default props for S3 Bucket| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|kinesisAnalytics()|[`kinesisAnalytics.CfnApplication`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kinesisanalytics.CfnApplication.html)|Returns an instance of the Kinesis Analytics application created by the pattern.| +|kinesisFirehose()|[`kinesisFirehose.CfnDeliveryStream`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kinesisfirehose.CfnDeliveryStream.html)|Returns an instance of the Kinesis Firehose delivery stream created by the pattern.| +|bucket()|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Returns an instance of the S3 bucket created by the pattern.| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/architecture.png new file mode 100644 index 000000000..9f9e472d8 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/lib/index.ts new file mode 100644 index 000000000..f89cbd4c8 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/lib/index.ts @@ -0,0 +1,128 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import * as kinesisFirehose from '@aws-cdk/aws-kinesisfirehose'; +import * as kinesisAnalytics from '@aws-cdk/aws-kinesisanalytics'; +import { KinesisFirehoseToS3, KinesisFirehoseToS3Props } from '@aws-solutions-konstruk/aws-kinesisfirehose-s3'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; + +/** + * The properties for the KinesisFirehoseToAnalyticsAndS3 class. + */ +export interface KinesisFirehoseToAnalyticsAndS3Props { + /** + * Optional user-provided props to override the default props for the Kinesis Firehose delivery stream. + * + * @default - Default props are used. + */ + readonly kinesisFirehoseProps?: kinesisFirehose.CfnDeliveryStreamProps | any; + /** + * Optional user-provided props to override the default props for the Kinesis Analytics application. + * + * @default - Default props are used. + */ + readonly kinesisAnalyticsProps?: kinesisAnalytics.CfnApplicationProps | any; + /** + * Whether to create a S3 Bucket or use an existing S3 Bucket. + * If set to false, you must provide S3 Bucket as `existingBucketObj` + * + * @default - true + */ + readonly deployBucket?: boolean, + /** + * Existing instance of S3 Bucket object. + * If `deployBucket` is set to false only then this property is required + * + * @default - None + */ + readonly existingBucketObj?: s3.Bucket, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly bucketProps?: s3.BucketProps +} + +/** + * @summary The KinesisFirehoseToAnalyticsAndS3 class. + */ +export class KinesisFirehoseToAnalyticsAndS3 extends Construct { + + // Declarations + private analytics: kinesisAnalytics.CfnApplication; + private kfs: KinesisFirehoseToS3; + + /** + * @summary Constructs a new instance of the KinesisFirehoseToAnalyticsAndS3 class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {CloudFrontToApiGatewayProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: KinesisFirehoseToAnalyticsAndS3Props) { + super(scope, id); + + // Setup the kinesisfirehose-s3 pattern + const kinesisFirehoseToS3Props: KinesisFirehoseToS3Props = { + kinesisFirehoseProps: props.kinesisFirehoseProps, + deployBucket: props.deployBucket, + existingBucketObj: props.existingBucketObj, + bucketProps: props.bucketProps + }; + + // Add the kinesisfirehose-s3 pattern + this.kfs = new KinesisFirehoseToS3(this, 'KinesisFirehoseToS3', kinesisFirehoseToS3Props); + + // Add the Kinesis Analytics application + this.analytics = defaults.buildKinesisAnalyticsApp(this, { + kinesisFirehose: this.kfs.kinesisFirehose(), + kinesisAnalyticsProps: props.kinesisAnalyticsProps + }); + } + + /** + * @summary Returns an instance of the kinesisAnalytics.CfnApplication created by the construct. + * @returns {kinesisAnalytics.CfnApplication} Instance of the CfnApplication created by the construct. + * @since 0.8.0 + * @access public + */ + public kinesisAnalytics(): kinesisAnalytics.CfnApplication { + return this.analytics; + } + + /** + * @summary Returns an instance of the kinesisFirehose.CfnDeliveryStream created by the construct. + * @returns {kinesisFirehose.CfnDeliveryStream} Instance of the CfnDeliveryStream created by the construct. + * @since 0.8.0 + * @access public + */ + public kinesisFirehose(): kinesisFirehose.CfnDeliveryStream { + return this.kfs.kinesisFirehose(); + } + + /** + * @summary Returns an instance of the s3.Bucket created by the construct. + * @returns {s3.Bucket} Instance of the Bucket created by the construct. + * @since 0.8.0 + * @access public + */ + public bucket(): s3.Bucket { + return this.kfs.bucket(); + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/package.json b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/package.json new file mode 100644 index 000000000..74fcbbb6d --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/package.json @@ -0,0 +1,84 @@ +{ + "name": "@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics", + "version": "0.8.0", + "description": "CDK constructs for defining an interaction between an Amazon Kinesis Data Firehose delivery stream and (1) an Amazon S3 bucket, and (2) an Amazon Kinesis Data Analytics application.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.kinesisfirehoses3kinesisanalytics", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "kinesisfirehoses3kinesisanalytics" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.KinesisFirehoseS3KinesisAnalytics", + "packageId": "Amazon.Konstruk.AWS.KinesisFirehoseS3KinesisAnalytics", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-kinesis-firehose-s3-kinesis-analytics", + "module": "aws_solutions_konstruk.aws_kinesis_firehose_s3_kinesis_analytics" + } + } + }, + "dependencies": { + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-kinesis": "~1.25.0", + "@aws-cdk/aws-kinesisanalytics": "~1.25.0", + "@aws-cdk/aws-kinesisfirehose": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-solutions-konstruk/aws-kinesisfirehose-s3": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-kinesis": "~1.25.0", + "@aws-cdk/aws-kinesisanalytics": "~1.25.0", + "@aws-cdk/aws-kinesisfirehose": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-solutions-konstruk/aws-kinesisfirehose-s3": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/__snapshots__/test.kinesisfirehose-analytics-s3.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/__snapshots__/test.kinesisfirehose-analytics-s3.test.js.snap new file mode 100644 index 000000000..f519732eb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/__snapshots__/test.kinesisfirehose-analytics-s3.test.js.snap @@ -0,0 +1,330 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pattern deployment w/ default properties 1`] = ` +Object { + "Resources": Object { + "testfirehoses3andanalyticsstackKinesisAnalytics20F3845E": Object { + "DependsOn": Array [ + "testfirehoses3andanalyticsstackKinesisAnalyticsPolicy2594304F", + ], + "Properties": Object { + "Inputs": Array [ + Object { + "InputSchema": Object { + "RecordColumns": Array [ + Object { + "Mapping": "$.ticker_symbol", + "Name": "ticker_symbol", + "SqlType": "VARCHAR(4)", + }, + Object { + "Mapping": "$.sector", + "Name": "sector", + "SqlType": "VARCHAR(16)", + }, + Object { + "Mapping": "$.change", + "Name": "change", + "SqlType": "REAL", + }, + Object { + "Mapping": "$.price", + "Name": "price", + "SqlType": "REAL", + }, + ], + "RecordEncoding": "UTF-8", + "RecordFormat": Object { + "RecordFormatType": "JSON", + }, + }, + "KinesisFirehoseInput": Object { + "ResourceARN": Object { + "Fn::GetAtt": Array [ + "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehose86F339C4", + "Arn", + ], + }, + "RoleARN": Object { + "Fn::GetAtt": Array [ + "testfirehoses3andanalyticsstackKinesisAnalyticsRole7217C4CC", + "Arn", + ], + }, + }, + "NamePrefix": "SOURCE_SQL_STREAM", + }, + ], + }, + "Type": "AWS::KinesisAnalytics::Application", + }, + "testfirehoses3andanalyticsstackKinesisAnalyticsPolicy2594304F": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "firehose:DescribeDeliveryStream", + "firehose:Get*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehose86F339C4", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "testfirehoses3andanalyticsstackKinesisAnalyticsPolicy2594304F", + "Roles": Array [ + Object { + "Ref": "testfirehoses3andanalyticsstackKinesisAnalyticsRole7217C4CC", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "testfirehoses3andanalyticsstackKinesisAnalyticsRole7217C4CC": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "kinesisanalytics.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehose86F339C4": Object { + "Properties": Object { + "ExtendedS3DestinationConfiguration": Object { + "BucketARN": Object { + "Fn::GetAtt": Array [ + "testfirehoses3andanalyticsstackKinesisFirehoseToS3S3BucketAE659354", + "Arn", + ], + }, + "BufferingHints": Object { + "IntervalInSeconds": 300, + "SizeInMBs": 5, + }, + "CloudWatchLoggingOptions": Object { + "Enabled": true, + "LogGroupName": Object { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroup7E569B76", + }, + "LogStreamName": Object { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroupfirehoselogstream98C70102", + }, + }, + "CompressionFormat": "GZIP", + "RoleARN": Object { + "Fn::GetAtt": Array [ + "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehoseRoleE7F8ADDA", + "Arn", + ], + }, + }, + }, + "Type": "AWS::KinesisFirehose::DeliveryStream", + }, + "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehosePolicy8E134001": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:AbortMultipartUpload", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:PutObject", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "testfirehoses3andanalyticsstackKinesisFirehoseToS3S3BucketAE659354", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "testfirehoses3andanalyticsstackKinesisFirehoseToS3S3BucketAE659354", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": "logs:PutLogEvents", + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:", + Object { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroup7E569B76", + }, + ":log-stream:", + Object { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroupfirehoselogstream98C70102", + }, + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehosePolicy8E134001", + "Roles": Array [ + Object { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehoseRoleE7F8ADDA", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehoseRoleE7F8ADDA": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "firehose.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "testfirehoses3andanalyticsstackKinesisFirehoseToS3S3BucketAE659354": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3S3LoggingBucket887A5000", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "testfirehoses3andanalyticsstackKinesisFirehoseToS3S3LoggingBucket887A5000": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroup7E569B76": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroupfirehoselogstream98C70102": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "LogGroupName": Object { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroup7E569B76", + }, + }, + "Type": "AWS::Logs::LogStream", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..20b9723a6 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/integ.no-arguments.expected.json @@ -0,0 +1,326 @@ +{ + "Resources": { + "testfirehoses3andanalyticsstackKinesisFirehoseToS3S3LoggingBucket887A5000": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + }, + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "testfirehoses3andanalyticsstackKinesisFirehoseToS3S3BucketAE659354": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3S3LoggingBucket887A5000" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroup7E569B76": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroupfirehoselogstream98C70102": { + "Type": "AWS::Logs::LogStream", + "Properties": { + "LogGroupName": { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroup7E569B76" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehoseRoleE7F8ADDA": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "firehose.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehosePolicy8E134001": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:AbortMultipartUpload", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:PutObject" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testfirehoses3andanalyticsstackKinesisFirehoseToS3S3BucketAE659354", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testfirehoses3andanalyticsstackKinesisFirehoseToS3S3BucketAE659354", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "logs:PutLogEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroup7E569B76" + }, + ":log-stream:", + { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroupfirehoselogstream98C70102" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehosePolicy8E134001", + "Roles": [ + { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehoseRoleE7F8ADDA" + } + ] + } + }, + "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehose86F339C4": { + "Type": "AWS::KinesisFirehose::DeliveryStream", + "Properties": { + "ExtendedS3DestinationConfiguration": { + "BucketARN": { + "Fn::GetAtt": [ + "testfirehoses3andanalyticsstackKinesisFirehoseToS3S3BucketAE659354", + "Arn" + ] + }, + "BufferingHints": { + "IntervalInSeconds": 300, + "SizeInMBs": 5 + }, + "CloudWatchLoggingOptions": { + "Enabled": true, + "LogGroupName": { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroup7E569B76" + }, + "LogStreamName": { + "Ref": "testfirehoses3andanalyticsstackKinesisFirehoseToS3firehoseloggroupfirehoselogstream98C70102" + } + }, + "CompressionFormat": "GZIP", + "RoleARN": { + "Fn::GetAtt": [ + "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehoseRoleE7F8ADDA", + "Arn" + ] + } + } + } + }, + "testfirehoses3andanalyticsstackKinesisAnalyticsRole7217C4CC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "kinesisanalytics.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testfirehoses3andanalyticsstackKinesisAnalyticsPolicy2594304F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "firehose:DescribeDeliveryStream", + "firehose:Get*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehose86F339C4", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testfirehoses3andanalyticsstackKinesisAnalyticsPolicy2594304F", + "Roles": [ + { + "Ref": "testfirehoses3andanalyticsstackKinesisAnalyticsRole7217C4CC" + } + ] + } + }, + "testfirehoses3andanalyticsstackKinesisAnalytics20F3845E": { + "Type": "AWS::KinesisAnalytics::Application", + "Properties": { + "Inputs": [ + { + "InputSchema": { + "RecordColumns": [ + { + "Mapping": "$.ticker_symbol", + "Name": "ticker_symbol", + "SqlType": "VARCHAR(4)" + }, + { + "Mapping": "$.sector", + "Name": "sector", + "SqlType": "VARCHAR(16)" + }, + { + "Mapping": "$.change", + "Name": "change", + "SqlType": "REAL" + }, + { + "Mapping": "$.price", + "Name": "price", + "SqlType": "REAL" + } + ], + "RecordEncoding": "UTF-8", + "RecordFormat": { + "RecordFormatType": "JSON" + } + }, + "KinesisFirehoseInput": { + "ResourceARN": { + "Fn::GetAtt": [ + "testfirehoses3andanalyticsstackKinesisFirehoseToS3KinesisFirehose86F339C4", + "Arn" + ] + }, + "RoleARN": { + "Fn::GetAtt": [ + "testfirehoses3andanalyticsstackKinesisAnalyticsRole7217C4CC", + "Arn" + ] + } + }, + "NamePrefix": "SOURCE_SQL_STREAM" + } + ] + }, + "DependsOn": [ + "testfirehoses3andanalyticsstackKinesisAnalyticsPolicy2594304F" + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/integ.no-arguments.ts new file mode 100644 index 000000000..ac3cd1476 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/integ.no-arguments.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { KinesisFirehoseToAnalyticsAndS3, KinesisFirehoseToAnalyticsAndS3Props } from "../lib"; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-firehose-s3-and-analytics-stack'); + +// Definitions +const props: KinesisFirehoseToAnalyticsAndS3Props = { + kinesisAnalyticsProps: { + inputs: [{ + inputSchema: { + recordColumns: [{ + name: 'ticker_symbol', + sqlType: 'VARCHAR(4)', + mapping: '$.ticker_symbol' + }, { + name: 'sector', + sqlType: 'VARCHAR(16)', + mapping: '$.sector' + }, { + name: 'change', + sqlType: 'REAL', + mapping: '$.change' + }, { + name: 'price', + sqlType: 'REAL', + mapping: '$.price' + }], + recordFormat: { + recordFormatType: 'JSON' + }, + recordEncoding: 'UTF-8' + }, + namePrefix: 'SOURCE_SQL_STREAM' + }] + } +}; + +new KinesisFirehoseToAnalyticsAndS3(stack, 'test-firehose-s3-and-analytics-stack', props); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/lambda/index.js new file mode 100644 index 000000000..5844e65a2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/lambda/index.js @@ -0,0 +1,8 @@ +exports.handler = async function(event) { + console.log('request:', JSON.stringify(event, undefined, 2)); + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: `//stub//` + }; + }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/test.kinesisfirehose-analytics-s3.test.ts b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/test.kinesisfirehose-analytics-s3.test.ts new file mode 100644 index 000000000..3386d44d7 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3-and-kinesisanalytics/test/test.kinesisfirehose-analytics-s3.test.ts @@ -0,0 +1,100 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { SynthUtils } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import { KinesisFirehoseToAnalyticsAndS3, KinesisFirehoseToAnalyticsAndS3Props } from '../lib'; +import '@aws-cdk/assert/jest'; + +// -------------------------------------------------------------- +// Test Case 1 - Pattern deployment w/ default properties +// -------------------------------------------------------------- +test('Pattern deployment w/ default properties', () => { + // Initial Setup + const stack = new Stack(); + const props: KinesisFirehoseToAnalyticsAndS3Props = { + kinesisAnalyticsProps: { + inputs: [{ + inputSchema: { + recordColumns: [{ + name: 'ticker_symbol', + sqlType: 'VARCHAR(4)', + mapping: '$.ticker_symbol' + }, { + name: 'sector', + sqlType: 'VARCHAR(16)', + mapping: '$.sector' + }, { + name: 'change', + sqlType: 'REAL', + mapping: '$.change' + }, { + name: 'price', + sqlType: 'REAL', + mapping: '$.price' + }], + recordFormat: { + recordFormatType: 'JSON' + }, + recordEncoding: 'UTF-8' + }, + namePrefix: 'SOURCE_SQL_STREAM' + }] + } + }; + new KinesisFirehoseToAnalyticsAndS3(stack, 'test-firehose-s3-and-analytics-stack', props); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test Case 2 - Test the getter methods +// -------------------------------------------------------------- +test('Test getter methods', () => { + // Initial Setup + const stack = new Stack(); + const props: KinesisFirehoseToAnalyticsAndS3Props = { + kinesisFirehoseProps: { + deploy: true, + props: { + deliveryStreamName: "myDeliveryStream" + } + }, + kinesisAnalyticsProps: { + inputs: [{ + inputSchema: { + recordColumns: [{ + name: 'ts', + sqlType: 'TIMESTAMP', + mapping: '$.timestamp' + }, { + name: 'trip_id', + sqlType: 'VARCHAR(64)', + mapping: '$.trip_id' + }], + recordFormat: { + recordFormatType: 'JSON' + }, + recordEncoding: 'UTF-8' + }, + namePrefix: 'SOURCE_SQL_STREAM' + }] + } + }; + const app = new KinesisFirehoseToAnalyticsAndS3(stack, 'test-kinesis-firehose-kinesis-analytics', props); + // Assertions + expect(app.kinesisAnalytics()).toBeDefined(); + expect(app.kinesisFirehose()).toBeDefined(); + expect(app.bucket()).toBeDefined(); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/README.md b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/README.md new file mode 100644 index 000000000..4c2f0aeb7 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/README.md @@ -0,0 +1,69 @@ +# aws-kinesisfirehose-s3 module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-kinesisfirehose-s3/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_kinesisfirehose_s3`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-kinesisfirehose-s3`| + +This AWS Solutions Konstruk implements an Amazon Kinesis Data Firehose delivery stream connected to an Amazon S3 bucket. + +Here is a minimal deployable pattern definition: + +``` javascript +const { KinesisFirehoseToS3 } = require('@aws-solutions-konstruk/aws-kinesisfirehose-s3'); + +new KinesisFirehoseToS3(stack, 'test-firehose-s3', {}); + +``` + +## Initializer + +``` text +new KinesisFirehoseToS3(scope: Construct, id: string, props: KinesisFirehoseToS3Props); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`KinesisFirehoseToS3Props`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|kinesisFirehoseProps?|[`kinesisfirehose.CfnDeliveryStreamProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kinesisfirehose.CfnDeliveryStreamProps.html)|Optional user provided props to override the default props for Kinesis Firehose Delivery Stream| +|deployBucket?|`boolean`|Whether to create a S3 Bucket or use an existing S3 Bucket| +|existingBucketObj?|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Existing instance of S3 Bucket object| +|bucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|Optional user provided props to override the default props for S3 Bucket| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|kinesisFirehose()|[`kinesisfirehose.CfnDeliveryStream`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kinesisfirehose.CfnDeliveryStream.html)|Retruns an instance of kinesisfirehose.CfnDeliveryStream created by the construct| +|bucket()|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Retruns an instance of s3.Bucket created by the construct| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/architecture.png new file mode 100644 index 000000000..93dbc0c56 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/lib/index.ts new file mode 100644 index 000000000..dd4988189 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/lib/index.ts @@ -0,0 +1,147 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as kinesisfirehose from '@aws-cdk/aws-kinesisfirehose'; +import { Construct } from '@aws-cdk/core'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as defaults from '@aws-solutions-konstruk/core'; +import * as iam from '@aws-cdk/aws-iam'; +import { overrideProps } from '@aws-solutions-konstruk/core'; +import * as logs from '@aws-cdk/aws-logs'; +import * as cdk from '@aws-cdk/core'; + +/** + * The properties for the KinesisFirehoseToS3 class. + */ +export interface KinesisFirehoseToS3Props { + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly kinesisFirehoseProps?: kinesisfirehose.CfnDeliveryStreamProps | any; + /** + * Whether to create a S3 Bucket or use an existing S3 Bucket. + * If set to false, you must provide S3 Bucket as `existingBucketObj` + * + * @default - true + */ + readonly deployBucket?: boolean, + /** + * Existing instance of S3 Bucket object. + * If `deployBucket` is set to false only then this property is required + * + * @default - None + */ + readonly existingBucketObj?: s3.Bucket, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly bucketProps?: s3.BucketProps +} + +export class KinesisFirehoseToS3 extends Construct { + + // Private variables + private firehose: kinesisfirehose.CfnDeliveryStream; + private s3Bucket: s3.Bucket; + + /** + * Constructs a new instance of the IotToLambda class. + */ + constructor(scope: Construct, id: string, props: KinesisFirehoseToS3Props) { + super(scope, id); + + // Setup S3 Bucket + this.s3Bucket = defaults.buildS3Bucket(this, { + deployBucket: props.deployBucket, + existingBucketObj: props.existingBucketObj, + bucketProps: props.bucketProps + }); + + // Extract the CfnBucket from the s3Bucket + const s3BucketResource = this.s3Bucket.node.findChild('Resource') as s3.CfnBucket; + + s3BucketResource.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W51', + reason: `This S3 bucket Bucket does not need a bucket policy` + }] + } + }; + + // Setup Cloudwatch Log group & stream for Kinesis Firehose + const cwLogGroup: logs.LogGroup = new logs.LogGroup(this, 'firehose-log-group', defaults.DefaultLogGroupProps()); + const cwLogStream: logs.LogStream = cwLogGroup.addStream('firehose-log-stream'); + + // Setup the IAM Role for Kinesis Firehose + const firehoseRole = new iam.Role(this, 'KinesisFirehoseRole', { + assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'), + }); + + // Setup the IAM policy for Kinesis Firehose + const firehosePolicy = new iam.Policy(this, 'KinesisFirehosePolicy', { + statements: [new iam.PolicyStatement({ + actions: [ + 's3:AbortMultipartUpload', + 's3:GetBucketLocation', + 's3:GetObject', + 's3:ListBucket', + 's3:ListBucketMultipartUploads', + 's3:PutObject' + ], + resources: [`${this.s3Bucket.bucketArn}`, `${this.s3Bucket.bucketArn}/*`] + }), + new iam.PolicyStatement({ + actions: [ + 'logs:PutLogEvents' + ], + resources: [`arn:aws:logs:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:log-group:${cwLogGroup.logGroupName}:log-stream:${cwLogStream.logStreamName}`] + }) + ]}); + + // Attach policy to role + firehosePolicy.attachToRole(firehoseRole); + + // Setup the default Kinesis Firehose props + const defaultKinesisFirehoseProps: kinesisfirehose.CfnDeliveryStreamProps = + defaults.DefaultCfnDeliveryStreamProps(this.s3Bucket.bucketArn, firehoseRole.roleArn, + cwLogGroup.logGroupName, cwLogStream.logStreamName); + + // Override with the input props + if (props.kinesisFirehoseProps) { + const kinesisFirehoseProps = overrideProps(defaultKinesisFirehoseProps, props.kinesisFirehoseProps); + this.firehose = new kinesisfirehose.CfnDeliveryStream(this, 'KinesisFirehose', kinesisFirehoseProps); + } else { + this.firehose = new kinesisfirehose.CfnDeliveryStream(this, 'KinesisFirehose', defaultKinesisFirehoseProps); + } + } + + /** + * Retruns an instance of kinesisfirehose.CfnDeliveryStream created by the construct + */ + public kinesisFirehose(): kinesisfirehose.CfnDeliveryStream { + return this.firehose; + } + + /** + * Retruns an instance of s3.Bucket created by the construct + */ + public bucket(): s3.Bucket { + return this.s3Bucket; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/package.json b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/package.json new file mode 100644 index 000000000..6379af08b --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/package.json @@ -0,0 +1,80 @@ +{ + "name": "@aws-solutions-konstruk/aws-kinesisfirehose-s3", + "version": "0.8.0", + "description": "CDK constructs for defining an interaction between an Amazon Kinesis Data Firehose delivery stream and an Amazon S3 bucket.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.kinesisfirehoses3", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "kinesisfirehoses3" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.KinesisFirehoseS3", + "packageId": "Amazon.Konstruk.AWS.KinesisFirehoseS3", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-kinesis-firehose-s3", + "module": "aws_solutions_konstruk.aws_kinesis_firehose_s3" + } + } + }, + "dependencies": { + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-kinesisfirehose": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-logs": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-kinesisfirehose": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-logs": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/test/__snapshots__/test.kinesisfirehose-s3.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/test/__snapshots__/test.kinesisfirehose-s3.test.js.snap new file mode 100644 index 000000000..77068fbc7 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/test/__snapshots__/test.kinesisfirehose-s3.test.js.snap @@ -0,0 +1,229 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test KinesisFirehoseToS3 default params 1`] = ` +Object { + "Resources": Object { + "testfirehoses3KinesisFirehose5D459661": Object { + "Properties": Object { + "ExtendedS3DestinationConfiguration": Object { + "BucketARN": Object { + "Fn::GetAtt": Array [ + "testfirehoses3S3Bucket93480488", + "Arn", + ], + }, + "BufferingHints": Object { + "IntervalInSeconds": 300, + "SizeInMBs": 5, + }, + "CloudWatchLoggingOptions": Object { + "Enabled": true, + "LogGroupName": Object { + "Ref": "testfirehoses3firehoseloggroup8067C3EC", + }, + "LogStreamName": Object { + "Ref": "testfirehoses3firehoseloggroupfirehoselogstreamAC5E7A6B", + }, + }, + "CompressionFormat": "GZIP", + "RoleARN": Object { + "Fn::GetAtt": Array [ + "testfirehoses3KinesisFirehoseRole9BC5362D", + "Arn", + ], + }, + }, + }, + "Type": "AWS::KinesisFirehose::DeliveryStream", + }, + "testfirehoses3KinesisFirehosePolicy34C2972F": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:AbortMultipartUpload", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:PutObject", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "testfirehoses3S3Bucket93480488", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "testfirehoses3S3Bucket93480488", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": "logs:PutLogEvents", + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:", + Object { + "Ref": "testfirehoses3firehoseloggroup8067C3EC", + }, + ":log-stream:", + Object { + "Ref": "testfirehoses3firehoseloggroupfirehoselogstreamAC5E7A6B", + }, + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "testfirehoses3KinesisFirehosePolicy34C2972F", + "Roles": Array [ + Object { + "Ref": "testfirehoses3KinesisFirehoseRole9BC5362D", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "testfirehoses3KinesisFirehoseRole9BC5362D": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "firehose.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "testfirehoses3S3Bucket93480488": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "testfirehoses3S3LoggingBucket31BFDC22", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "testfirehoses3S3LoggingBucket31BFDC22": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "testfirehoses3firehoseloggroup8067C3EC": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "testfirehoses3firehoseloggroupfirehoselogstreamAC5E7A6B": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "LogGroupName": Object { + "Ref": "testfirehoses3firehoseloggroup8067C3EC", + }, + }, + "Type": "AWS::Logs::LogStream", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..d8ff5194c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/test/integ.no-arguments.expected.json @@ -0,0 +1,226 @@ +{ + "Description": "Integration Test for aws-cdk-apl-kinesisfirehose-s3", + "Resources": { + "testfirehoses3S3LoggingBucket31BFDC22": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + }, + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "testfirehoses3S3Bucket93480488": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "testfirehoses3S3LoggingBucket31BFDC22" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "testfirehoses3firehoseloggroup8067C3EC": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testfirehoses3firehoseloggroupfirehoselogstreamAC5E7A6B": { + "Type": "AWS::Logs::LogStream", + "Properties": { + "LogGroupName": { + "Ref": "testfirehoses3firehoseloggroup8067C3EC" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testfirehoses3KinesisFirehoseRole9BC5362D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "firehose.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testfirehoses3KinesisFirehosePolicy34C2972F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:AbortMultipartUpload", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:PutObject" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testfirehoses3S3Bucket93480488", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testfirehoses3S3Bucket93480488", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "logs:PutLogEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "testfirehoses3firehoseloggroup8067C3EC" + }, + ":log-stream:", + { + "Ref": "testfirehoses3firehoseloggroupfirehoselogstreamAC5E7A6B" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testfirehoses3KinesisFirehosePolicy34C2972F", + "Roles": [ + { + "Ref": "testfirehoses3KinesisFirehoseRole9BC5362D" + } + ] + } + }, + "testfirehoses3KinesisFirehose5D459661": { + "Type": "AWS::KinesisFirehose::DeliveryStream", + "Properties": { + "ExtendedS3DestinationConfiguration": { + "BucketARN": { + "Fn::GetAtt": [ + "testfirehoses3S3Bucket93480488", + "Arn" + ] + }, + "BufferingHints": { + "IntervalInSeconds": 300, + "SizeInMBs": 5 + }, + "CloudWatchLoggingOptions": { + "Enabled": true, + "LogGroupName": { + "Ref": "testfirehoses3firehoseloggroup8067C3EC" + }, + "LogStreamName": { + "Ref": "testfirehoses3firehoseloggroupfirehoselogstreamAC5E7A6B" + } + }, + "CompressionFormat": "GZIP", + "RoleARN": { + "Fn::GetAtt": [ + "testfirehoses3KinesisFirehoseRole9BC5362D", + "Arn" + ] + } + } + } + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/test/integ.no-arguments.ts new file mode 100644 index 000000000..62158133e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/test/integ.no-arguments.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { KinesisFirehoseToS3 } from "../lib"; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-firehose-s3-stack'); +stack.templateOptions.description = 'Integration Test for aws-cdk-apl-kinesisfirehose-s3'; + +new KinesisFirehoseToS3(stack, 'test-firehose-s3', {}); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/test/test.kinesisfirehose-s3.test.ts b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/test/test.kinesisfirehose-s3.test.ts new file mode 100644 index 000000000..e62d545ac --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisfirehose-s3/test/test.kinesisfirehose-s3.test.ts @@ -0,0 +1,113 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { KinesisFirehoseToS3, KinesisFirehoseToS3Props } from "../lib"; +import * as cdk from '@aws-cdk/core'; +import * as kinesisfirehose from '@aws-cdk/aws-kinesisfirehose'; +import '@aws-cdk/assert/jest'; + +function deploy(stack: cdk.Stack) { + const props = {} as KinesisFirehoseToS3Props; + + return new KinesisFirehoseToS3(stack, 'test-firehose-s3', props); +} + +test('snapshot test KinesisFirehoseToS3 default params', () => { + const stack = new cdk.Stack(); + deploy(stack); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check s3Bucket default encryption', () => { + const stack = new cdk.Stack(); + deploy(stack); + expect(stack).toHaveResource('AWS::S3::Bucket', { + BucketEncryption: { + ServerSideEncryptionConfiguration: [{ + ServerSideEncryptionByDefault : { + SSEAlgorithm: "AES256" + } + }] + } + }); +}); + +test('check s3Bucket public access block configuration', () => { + const stack = new cdk.Stack(); + deploy(stack); + expect(stack).toHaveResource('AWS::S3::Bucket', { + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true + } + }); +}); + +test('test s3Bucket override publicAccessBlockConfiguration', () => { + const stack = new cdk.Stack(); + + new KinesisFirehoseToS3(stack, 'test-firehose-s3', { + bucketProps: { + blockPublicAccess: { + blockPublicAcls: false, + blockPublicPolicy: true, + ignorePublicAcls: false, + restrictPublicBuckets: true + } + } + }); + + expect(stack).toHaveResource("AWS::S3::Bucket", { + PublicAccessBlockConfiguration: { + BlockPublicAcls: false, + BlockPublicPolicy: true, + IgnorePublicAcls: false, + RestrictPublicBuckets: true + }, + }); +}); + +test('test kinesisFirehose override ', () => { + const stack = new cdk.Stack(); + + new KinesisFirehoseToS3(stack, 'test-firehose-s3', { + kinesisFirehoseProps: { + extendedS3DestinationConfiguration: { + bufferingHints: { + intervalInSeconds: 600, + sizeInMBs: 55 + }, + } + } + }); + + expect(stack).toHaveResourceLike("AWS::KinesisFirehose::DeliveryStream", { + ExtendedS3DestinationConfiguration: { + BufferingHints: { + IntervalInSeconds: 600, + SizeInMBs: 55 + } + }}); +}); + +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: KinesisFirehoseToS3 = deploy(stack); + + expect(construct.kinesisFirehose()).toBeInstanceOf(kinesisfirehose.CfnDeliveryStream); + expect(construct.bucket()).toBeDefined(); +}); diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/README.md b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/README.md new file mode 100644 index 000000000..d70f9de0d --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/README.md @@ -0,0 +1,82 @@ +# aws-kinesisstreams-lambda module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-kinesisstreams-lambda/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_kinesisstreams_lambda`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-kinesisstreams-lambda`| + +This AWS Solutions Konstruk deploys a Kinesis Stream and Lambda function with the appropriate resources/properties for interaction and security. + +Here is a minimal deployable pattern definition: + +``` javascript +const { KinesisStreamsToLambda } = require('@aws-solutions-konstruk/aws-kinesisstreams-lambda'); + +new KinesisStreamsToLambda(stack, 'KinesisToLambdaPattern', { + deployLambda: true, + eventSourceProps: { + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + batchSize: 1 + }, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } +}); + +``` + +## Initializer + +``` text +new KinesisStreamsToLambda(scope: Construct, id: string, props: KinesisStreamsToLambdaProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`KinesisStreamsToLambdaProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function. If set to false, you must provide an existing function for the `existingLambdaObj` property.| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|An optional, existing Lambda function. This property is required if `deployLambda` is set to false.| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user-provided props to override the default props for the Lambda function. This property is only required if `deployLambda` is set to true.| +|kinesisStreamProps?|[`kinesis.StreamProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kinesis.StreamProps.html)|Optional user-provided props to override the default props for the Kinesis stream.| +|eventSourceProps?|[`lambda.EventSourceMappingOptions`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.EventSourceMappingOptions.html)|Optional user-provided props to override the default props for the Lambda event source mapping.| +|encryptionKeyProps?|[`kms.KeyProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kms.KeyProps.html)|Optional user-provided props to override the default props for the KMS encryption key.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|stream()|[`kinesis.Stream`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kinesis.Stream.html)|Returns an instance of the Kinesis stream created by the pattern.| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Returns an instance of the Lambda function created by the pattern.| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/architecture.png new file mode 100644 index 000000000..fab430632 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/lib/index.ts new file mode 100644 index 000000000..544a81b5f --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/lib/index.ts @@ -0,0 +1,167 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import * as lambda from '@aws-cdk/aws-lambda'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as kms from '@aws-cdk/aws-kms'; +import * as iam from '@aws-cdk/aws-iam'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { overrideProps } from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; + +/** + * The properties for the KinesisStreamsToLambda class. + */ +export interface KinesisStreamsToLambdaProps { + /** + * Whether to create a new Lambda function or use an existing Lambda function. + * If set to false, you must provide an existing function for the `existingLambdaObj` property. + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * An optional, existing Lambda function. + * This property is required if `deployLambda` is set to false. + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user-provided props to override the default props for the Lambda function. + * This property is only required if `deployLambda` is set to true. + * + * @default - Default props are used. + */ + readonly lambdaFunctionProps?: lambda.FunctionProps | any + /** + * Optional user-provided props to override the default props for the Kinesis stream. + * + * @default - Default props are used. + */ + readonly kinesisStreamProps?: kinesis.StreamProps | any + /** + * Optional user-provided props to override the default props for the Lambda event source mapping. + * + * @default - Default props are used. + */ + readonly eventSourceProps?: lambda.EventSourceMappingOptions | any + /** + * Optional user-provided props to override the default props for the KMS encryption key. + * + * @default - Default props are used. + */ + readonly encryptionKeyProps?: kms.KeyProps | any +} + +/** + * @summary The KinesisStreamsToLambda class. + */ +export class KinesisStreamsToLambda extends Construct { + // Private variables + private kinesisStream: kinesis.Stream; + private fn: lambda.Function; + private encryptionKey: kms.Key; + + /** + * @summary Constructs a new instance of the KinesisStreamsToLambda class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {CloudFrontToApiGatewayProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: KinesisStreamsToLambdaProps) { + super(scope, id); + + // Setup the encryption key + this.encryptionKey = defaults.buildEncryptionKey(scope, { + encryptionKeyProps: props.encryptionKeyProps + }); + + // Setup the Kinesis Stream + this.kinesisStream = defaults.buildKinesisStream(scope, { + encryptionKey: this.encryptionKey, + kinesisStreamProps: props.kinesisStreamProps + }); + + // Setup the Lambda function + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + + // Add the Lambda event source mapping + const eventSourceProps = overrideProps(defaults.DefaultKinesisEventSourceProps, props.eventSourceProps); + eventSourceProps.eventSourceArn = this.kinesisStream.streamArn; + eventSourceProps.functionName = this.fn.functionName; + this.fn.addEventSourceMapping('LambdaKinesisEventSourceMapping', eventSourceProps); + + // Add permissions for the Lambda function to access Kinesis + const policy = new iam.Policy(this, 'LambdaFunctionPolicy'); + const role = this.fn.role as iam.Role; + policy.addStatements(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + resources: [ this.kinesisStream.streamArn ], + actions: [ + 'kinesis:GetRecords', + 'kinesis:GetShardIterator', + 'kinesis:DescribeStream' + ] + })); + policy.addStatements(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + resources: [ '*' ], + actions: [ + 'kinesis:ListStreams', + ] + })); + policy.attachToRole(role); + this.kinesisStream.grantRead(this.fn.grantPrincipal); + + // Add appropriate cfn_nag metadata + const cfnCustomPolicy = policy.node.defaultChild as iam.CfnPolicy; + cfnCustomPolicy.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W12", + reason: "The kinesis:ListStreams action requires a wildcard resource." + } + ] + } + }; + } + + /** + * @summary Returns an instance of the kinesis.Stream created by the construct. + * @returns {kinesis.Stream} Instance of the Stream created by the construct. + * @since 0.8.0 + * @access public + */ + public stream(): kinesis.Stream { + return this.kinesisStream; + } + + /** + * @summary Returns an instance of the lambda.Function created by the construct. + * @returns {lambda.Function} Instance of the Function created by the construct. + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/package.json b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/package.json new file mode 100644 index 000000000..6e65b26bb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/package.json @@ -0,0 +1,81 @@ +{ + "name": "@aws-solutions-konstruk/aws-kinesisstreams-lambda", + "version": "0.8.0", + "description": "CDK constructs for defining an interaction between an Amazon Kinesis Data Stream and an AWS Lambda function.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.kinesisstreamslambda", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "kinesisstreamslambda" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.KinesisStreamsLambda", + "packageId": "Amazon.Konstruk.AWS.KinesisStreamsLambda", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-kinesis-streams-lambda", + "module": "aws_solutions_konstruk.aws_kinesis_streams_lambda" + } + } + }, + "dependencies": { + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-kinesis": "~1.25.0", + "@aws-cdk/aws-kms": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-kinesis": "~1.25.0", + "@aws-cdk/aws-kms": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/__snapshots__/test.kinesisstreams-lambda.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/__snapshots__/test.kinesisstreams-lambda.test.js.snap new file mode 100644 index 000000000..7fdd4bd79 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/__snapshots__/test.kinesisstreams-lambda.test.js.snap @@ -0,0 +1,325 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pattern deployment 1`] = ` +Object { + "Parameters": Object { + "AssetParametersdfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcdArtifactHashEA3A5944": Object { + "Description": "Artifact hash for asset \\"dfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcd\\"", + "Type": "String", + }, + "AssetParametersdfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcdS3BucketA460830B": Object { + "Description": "S3 bucket for asset \\"dfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcd\\"", + "Type": "String", + }, + "AssetParametersdfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcdS3VersionKey58FEB9E6": Object { + "Description": "S3 key for asset version \\"dfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcd\\"", + "Type": "String", + }, + }, + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + Object { + "Action": "kms:Decrypt", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "KinesisStream46752A3E": Object { + "Properties": Object { + "RetentionPeriodHours": 24, + "ShardCount": 1, + "StreamEncryption": Object { + "EncryptionType": "KMS", + "KeyId": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + }, + "Type": "AWS::Kinesis::Stream", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParametersdfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcdS3BucketA460830B", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersdfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcdS3VersionKey58FEB9E6", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersdfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcdS3VersionKey58FEB9E6", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionLambdaKinesisEventSourceMappingE4E81D45": Object { + "Properties": Object { + "BatchSize": 1, + "EventSourceArn": Object { + "Fn::GetAtt": Array [ + "KinesisStream46752A3E", + "Arn", + ], + }, + "FunctionName": Object { + "Ref": "LambdaFunctionBF21E41F", + }, + "StartingPosition": "TRIM_HORIZON", + }, + "Type": "AWS::Lambda::EventSourceMapping", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kinesis:DescribeStream", + "kinesis:GetRecords", + "kinesis:GetShardIterator", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "KinesisStream46752A3E", + "Arn", + ], + }, + }, + Object { + "Action": "kms:Decrypt", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "testkinesisstreamslambdaLambdaFunctionPolicyF7EF016E": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "The kinesis:ListStreams action requires a wildcard resource.", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kinesis:GetRecords", + "kinesis:GetShardIterator", + "kinesis:DescribeStream", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "KinesisStream46752A3E", + "Arn", + ], + }, + }, + Object { + "Action": "kinesis:ListStreams", + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "testkinesisstreamslambdaLambdaFunctionPolicyF7EF016E", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/integ.deployFunction.expected.json b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/integ.deployFunction.expected.json new file mode 100644 index 000000000..f0cce5a64 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/integ.deployFunction.expected.json @@ -0,0 +1,322 @@ +{ + "Description": "Integration Test for aws-kinesisstreams-lambda", + "Resources": { + "testkslambdaLambdaFunctionPolicyDC40446F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kinesis:GetRecords", + "kinesis:GetShardIterator", + "kinesis:DescribeStream" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KinesisStream46752A3E", + "Arn" + ] + } + }, + { + "Action": "kinesis:ListStreams", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testkslambdaLambdaFunctionPolicyDC40446F", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "The kinesis:ListStreams action requires a wildcard resource." + } + ] + } + } + }, + "EncryptionKey1B843E66": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": "kms:Decrypt", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "EnableKeyRotation": true + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "KinesisStream46752A3E": { + "Type": "AWS::Kinesis::Stream", + "Properties": { + "ShardCount": 1, + "RetentionPeriodHours": 24, + "StreamEncryption": { + "EncryptionType": "KMS", + "KeyId": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + } + } + } + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kinesis:DescribeStream", + "kinesis:GetRecords", + "kinesis:GetShardIterator" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "KinesisStream46752A3E", + "Arn" + ] + } + }, + { + "Action": "kms:Decrypt", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersdfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcdS3BucketA460830B" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersdfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcdS3VersionKey58FEB9E6" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersdfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcdS3VersionKey58FEB9E6" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "LambdaFunctionLambdaKinesisEventSourceMappingE4E81D45": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "EventSourceArn": { + "Fn::GetAtt": [ + "KinesisStream46752A3E", + "Arn" + ] + }, + "FunctionName": { + "Ref": "LambdaFunctionBF21E41F" + }, + "BatchSize": 1, + "StartingPosition": "TRIM_HORIZON" + } + } + }, + "Parameters": { + "AssetParametersdfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcdS3BucketA460830B": { + "Type": "String", + "Description": "S3 bucket for asset \"dfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcd\"" + }, + "AssetParametersdfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcdS3VersionKey58FEB9E6": { + "Type": "String", + "Description": "S3 key for asset version \"dfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcd\"" + }, + "AssetParametersdfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcdArtifactHashEA3A5944": { + "Type": "String", + "Description": "Artifact hash for asset \"dfe828a7d00b0da7a6e92dc1decf39ec907e4edc6006faea8631d4dabd7f4fcd\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/integ.deployFunction.ts b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/integ.deployFunction.ts new file mode 100644 index 000000000..d04854a6d --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/integ.deployFunction.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { KinesisStreamsToLambda, KinesisStreamsToLambdaProps } from '../lib'; +import { Stack, App } from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-ks-lambda-stack'); +stack.templateOptions.description = 'Integration Test for aws-kinesisstreams-lambda'; + +// Definitions +const props: KinesisStreamsToLambdaProps = { + deployLambda: true, + encryptionKeyProps: {}, + kinesisStreamProps: {}, + eventSourceProps: { + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + batchSize: 1 + }, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, +}; + +new KinesisStreamsToLambda(stack, 'test-ks-lambda', props); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/lambda/index.js new file mode 100644 index 000000000..f63f72c0c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/lambda/index.js @@ -0,0 +1,21 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +exports.handler = async function(event) { + console.log('request:', JSON.stringify(event, undefined, 2)); + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: `//stub//` + }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/test.kinesisstreams-lambda.test.ts b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/test.kinesisstreams-lambda.test.ts new file mode 100644 index 000000000..851b35768 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-kinesisstreams-lambda/test/test.kinesisstreams-lambda.test.ts @@ -0,0 +1,72 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { Stack } from "@aws-cdk/core"; +import { KinesisStreamsToLambda, KinesisStreamsToLambdaProps } from "../lib"; +import { StartingPosition } from '@aws-cdk/aws-lambda'; +import { SynthUtils } from '@aws-cdk/assert'; +import * as lambda from '@aws-cdk/aws-lambda'; +import '@aws-cdk/assert/jest'; + +// -------------------------------------------------------------- +// Pattern deployment +// -------------------------------------------------------------- +test('Pattern deployment', () => { + // Initial setup + const stack = new Stack(); + const props: KinesisStreamsToLambdaProps = { + encryptionKeyProps: {}, + kinesisStreamProps: {}, + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + eventSourceProps: { + startingPosition: StartingPosition.TRIM_HORIZON, + batchSize: 1 + } + }; + new KinesisStreamsToLambda(stack, 'test-kinesis-streams-lambda', props); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test getter methods +// -------------------------------------------------------------- +test('Test getter methods', () => { + // Initial Setup + const stack = new Stack(); + const props: KinesisStreamsToLambdaProps = { + encryptionKeyProps: {}, + kinesisStreamProps: {}, + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + eventSourceProps: { + startingPosition: StartingPosition.TRIM_HORIZON, + batchSize: 1 + } + }; + const app = new KinesisStreamsToLambda(stack, 'test-kinesis-streams-lambda', props); + // Assertion 1 + expect(app.lambdaFunction()).toBeDefined(); + // Assertion 2 + expect(app.stream()).toBeDefined(); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/README.md b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/README.md new file mode 100644 index 000000000..58e0f28df --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/README.md @@ -0,0 +1,78 @@ +# aws-lambda-dynamodb module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-lambda-dynamodb/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_lambda_dynamodb`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-lambda-dynamodb`| + +This AWS Solutions Konstruk implements the AWS Lambda function and Amazon DynamoDB table with the least privileged permissions. + +Here is a minimal deployable pattern definition: + +``` javascript +const { LambdaToDynamoDBProps, LambdaToDynamoDB } = require('@aws-solutions-konstruk/aws-lambda-dynamodb'); + +const props: LambdaToDynamoDBProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, +}; + +new LambdaToDynamoDB(stack, 'test-lambda-dynamodb-stack', props); + +``` + +## Initializer + +``` text +new LambdaToDynamoDB(scope: Construct, id: string, props: LambdaToDynamoDBProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`LambdaToDynamoDBProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Existing instance of Lambda Function object| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user provided props to override the default props for Lambda function| +|dynamoTableProps|[`dynamodb.TableProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.TableProps.html)|Optional user provided props to override the default props for DynamoDB Table| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Retruns an instance of lambda.Function created by the construct| +|dynamoTable()|[`dynamodb.Table`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.Table.html)|Retruns an instance of dynamodb.Table created by the construct| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/architecture.png new file mode 100644 index 000000000..710b68e54 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/lib/index.ts new file mode 100644 index 000000000..8832735ec --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/lib/index.ts @@ -0,0 +1,107 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as lambda from '@aws-cdk/aws-lambda'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; +import { overrideProps } from '@aws-solutions-konstruk/core'; + +/** + * @summary The properties for the LambdaToDynamoDB Construct + */ +export interface LambdaToDynamoDBProps { + /** + * Whether to create a new lambda function or use an existing lambda function. + * If set to false, you must provide a lambda function object as `existingObj` + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly lambdaFunctionProps?: lambda.FunctionProps, + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly dynamoTableProps?: dynamodb.TableProps +} + +export class LambdaToDynamoDB extends Construct { + private fn: lambda.Function; + private table: dynamodb.Table; + + /** + * @summary Constructs a new instance of the LambdaToDynamoDB class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {LambdaToDynamoDBProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: LambdaToDynamoDBProps) { + super(scope, id); + + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + + // Set the default props for DynamoDB table + if (props.dynamoTableProps) { + const dynamoTableProps = overrideProps(defaults.DefaultTableProps, props.dynamoTableProps); + this.table = new dynamodb.Table(this, 'DynamoTable', dynamoTableProps); + } else { + this.table = new dynamodb.Table(this, 'DynamoTable', defaults.DefaultTableProps); + } + + this.fn.addEnvironment('DDB_TABLE_NAME', this.table.tableName); + + this.table.grantReadWriteData(this.fn.grantPrincipal); + } + + /** + * @summary Retruns an instance of dynamodb.Table created by the construct. + * @returns {dynamodb.Table} Instance of dynamodb.Table created by the construct + * @since 0.8.0 + * @access public + */ + public dynamoTable(): dynamodb.Table { + return this.table; + } + + /** + * @summary Retruns an instance of lambda.Function created by the construct. + * @returns {lambda.Function} Instance of lambda.Function created by the construct + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } + +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/package.json b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/package.json new file mode 100644 index 000000000..cb43a8a40 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/package.json @@ -0,0 +1,77 @@ +{ + "name": "@aws-solutions-konstruk/aws-lambda-dynamodb", + "version": "0.8.0", + "description": "CDK Constructs for AWS Lambda to AWS DynamoDB integration.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.lambdadynamodb", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "lambdadynamodb" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.LambdaDynamodb", + "packageId": "Amazon.Konstruk.AWS.LambdaDynamodb", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-lambda-dynamodb", + "module": "aws_solutions_konstruk.aws_lambda_dynamodb" + } + } + }, + "dependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-dynamodb": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-dynamodb": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/__snapshots__/lambda-dynamodb.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/__snapshots__/lambda-dynamodb.test.js.snap new file mode 100644 index 000000000..5c8fbe976 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/__snapshots__/lambda-dynamodb.test.js.snap @@ -0,0 +1,211 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test LambdaToDynamoDB default params 1`] = ` +Object { + "Parameters": Object { + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8ArtifactHash8D9AD644": Object { + "Description": "Artifact hash for asset \\"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\\"", + "Type": "String", + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB": Object { + "Description": "S3 bucket for asset \\"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\\"", + "Type": "String", + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7": Object { + "Description": "S3 key for asset version \\"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": Object { + "Ref": "testlambdadynamodbstackDynamoTable8138E93B", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "testlambdadynamodbstackDynamoTable8138E93B", + "Arn", + ], + }, + Object { + "Ref": "AWS::NoValue", + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "testlambdadynamodbstackDynamoTable8138E93B": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "AttributeDefinitions": Array [ + Object { + "AttributeName": "id", + "AttributeType": "S", + }, + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": Array [ + Object { + "AttributeName": "id", + "KeyType": "HASH", + }, + ], + "SSESpecification": Object { + "SSEEnabled": true, + }, + }, + "Type": "AWS::DynamoDB::Table", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.add-secondary-index.expected.json b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.add-secondary-index.expected.json new file mode 100644 index 000000000..7f5c82981 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.add-secondary-index.expected.json @@ -0,0 +1,236 @@ +{ + "Resources": { + "testlambdadynamodbstackDynamoTable8138E93B": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + }, + { + "AttributeName": "id2", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "GlobalSecondaryIndexes": [ + { + "IndexName": "test_id2", + "KeySchema": [ + { + "AttributeName": "id2", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + } + ], + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlambdadynamodbstackDynamoTable8138E93B", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testlambdadynamodbstackDynamoTable8138E93B", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": { + "Ref": "testlambdadynamodbstackDynamoTable8138E93B" + } + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + } + }, + "Parameters": { + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB": { + "Type": "String", + "Description": "S3 bucket for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7": { + "Type": "String", + "Description": "S3 key for asset version \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8ArtifactHash8D9AD644": { + "Type": "String", + "Description": "Artifact hash for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.add-secondary-index.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.add-secondary-index.ts new file mode 100644 index 000000000..127960e45 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.add-secondary-index.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { LambdaToDynamoDB } from "../lib"; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as lambda from '@aws-cdk/aws-lambda'; +const app = new App(); + +// Change the billing mode to PROVISIONED +const stack = new Stack(app, 'test-lambda-dynamodb-stack'); + +const construct: LambdaToDynamoDB = new LambdaToDynamoDB(stack, 'test-lambda-dynamodb-stack', { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }, +}); + +const props: dynamodb.GlobalSecondaryIndexProps = { + partitionKey: { + name: 'id2', + type: dynamodb.AttributeType.STRING + }, + indexName: 'test_id2' +}; +construct.dynamoTable().addGlobalSecondaryIndex(props); + +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..a95e1b63f --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.no-arguments.expected.json @@ -0,0 +1,207 @@ +{ + "Resources": { + "testlambdadynamodbstackDynamoTable8138E93B": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlambdadynamodbstackDynamoTable8138E93B", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": { + "Ref": "testlambdadynamodbstackDynamoTable8138E93B" + } + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + } + }, + "Parameters": { + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB": { + "Type": "String", + "Description": "S3 bucket for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7": { + "Type": "String", + "Description": "S3 key for asset version \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8ArtifactHash8D9AD644": { + "Type": "String", + "Description": "Artifact hash for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.no-arguments.ts new file mode 100644 index 000000000..26a4e3048 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.no-arguments.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { LambdaToDynamoDB, LambdaToDynamoDBProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +const app = new App(); + +// Empty arguments +const stack = new Stack(app, 'test-lambda-dynamodb-stack'); + +const props: LambdaToDynamoDBProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }, +}; + +new LambdaToDynamoDB(stack, 'test-lambda-dynamodb-stack', props); +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.set-billing-mode.expected.json b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.set-billing-mode.expected.json new file mode 100644 index 000000000..587990468 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.set-billing-mode.expected.json @@ -0,0 +1,210 @@ +{ + "Resources": { + "testlambdadynamodbstackDynamoTable8138E93B": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 3, + "WriteCapacityUnits": 3 + }, + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlambdadynamodbstackDynamoTable8138E93B", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": { + "Ref": "testlambdadynamodbstackDynamoTable8138E93B" + } + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + } + }, + "Parameters": { + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB": { + "Type": "String", + "Description": "S3 bucket for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7": { + "Type": "String", + "Description": "S3 key for asset version \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8ArtifactHash8D9AD644": { + "Type": "String", + "Description": "Artifact hash for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.set-billing-mode.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.set-billing-mode.ts new file mode 100644 index 000000000..9e38e89dc --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.set-billing-mode.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { LambdaToDynamoDB } from "../lib"; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as lambda from '@aws-cdk/aws-lambda'; +const app = new App(); + +// Change the billing mode to PROVISIONED +const stack = new Stack(app, 'test-lambda-dynamodb-stack'); + +new LambdaToDynamoDB(stack, 'test-lambda-dynamodb-stack', { + dynamoTableProps: { + billingMode: dynamodb.BillingMode.PROVISIONED, + readCapacity: 3, + writeCapacity: 3, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + } + }, + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + } +}); + +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.use-existing-func.expected.json b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.use-existing-func.expected.json new file mode 100644 index 000000000..499220ebc --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.use-existing-func.expected.json @@ -0,0 +1,207 @@ +{ + "Resources": { + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlambdadynamodbstackDynamoTable8138E93B", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": { + "Ref": "testlambdadynamodbstackDynamoTable8138E93B" + } + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "testlambdadynamodbstackDynamoTable8138E93B": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + } + }, + "Parameters": { + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB": { + "Type": "String", + "Description": "S3 bucket for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7": { + "Type": "String", + "Description": "S3 key for asset version \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8ArtifactHash8D9AD644": { + "Type": "String", + "Description": "Artifact hash for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.use-existing-func.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.use-existing-func.ts new file mode 100644 index 000000000..203a9cef5 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/integ.use-existing-func.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { LambdaToDynamoDB } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as defaults from '@aws-solutions-konstruk/core'; + +const app = new App(); +const stack = new Stack(app, 'test-lambda-dynamodb-stack'); +const lambdaFunctionProps = { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) +}; + +const func = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + +new LambdaToDynamoDB(stack, 'test-lambda-dynamodb-stack', { + deployLambda: false, + existingLambdaObj: func +}); + +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/lambda-dynamodb.test.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/lambda-dynamodb.test.ts new file mode 100644 index 000000000..8952d2223 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/lambda-dynamodb.test.ts @@ -0,0 +1,307 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { LambdaToDynamoDB, LambdaToDynamoDBProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as cdk from "@aws-cdk/core"; +import '@aws-cdk/assert/jest'; + +function deployNewFunc(stack: cdk.Stack) { + const props: LambdaToDynamoDBProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }, + }; + + return new LambdaToDynamoDB(stack, 'test-lambda-dynamodb-stack', props); +} + +function useExistingFunc(stack: cdk.Stack) { + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.PYTHON_3_6, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + const props: LambdaToDynamoDBProps = { + deployLambda: false, + existingLambdaObj: new lambda.Function(stack, 'MyExistingFunction', lambdaFunctionProps), + dynamoTableProps: { + billingMode: dynamodb.BillingMode.PROVISIONED, + readCapacity: 3, + writeCapacity: 3, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + } + }, + }; + + return new LambdaToDynamoDB(stack, 'test-lambda-dynamodb-stack', props); +} + +test('snapshot test LambdaToDynamoDB default params', () => { + const stack = new cdk.Stack(); + deployNewFunc(stack); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check lambda function properties for deploy: true', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + Runtime: "nodejs10.x", + Environment: { + Variables: { + AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1", + DDB_TABLE_NAME: { + Ref: "testlambdadynamodbstackDynamoTable8138E93B" + } + } + } + }); +}); +test('check dynamo table properties for deploy: true', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::DynamoDB::Table', { + KeySchema: [ + { + AttributeName: "id", + KeyType: "HASH" + } + ], + AttributeDefinitions: [ + { + AttributeName: "id", + AttributeType: "S" + } + ], + BillingMode: "PAY_PER_REQUEST", + SSESpecification: { + SSEEnabled: true + } + }); +}); +test('check iot lambda function role for deploy: true', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: "lambda.amazonaws.com" + } + } + ], + Version: "2012-10-17" + }, + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + PolicyName: "LambdaFunctionServiceRolePolicy" + } + ] + }); +}); +test('check lambda function policy for deploy: true', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem" + ], + Effect: "Allow", + Resource: [ + { + "Fn::GetAtt": [ + "testlambdadynamodbstackDynamoTable8138E93B", + "Arn" + ] + }, + { + Ref: "AWS::NoValue" + } + ] + } + ], + Version: "2012-10-17" + } + }); +}); +test('check lambda function properties for deploy: false', () => { + const stack = new cdk.Stack(); + + useExistingFunc(stack); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "MyExistingFunctionServiceRoleF9E14BFD", + "Arn" + ] + }, + Runtime: "python3.6" + }); +}); +test('check iot lambda function role for deploy: false', () => { + const stack = new cdk.Stack(); + + useExistingFunc(stack); + + expect(stack).toHaveResource('AWS::IAM::Role', { + 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" + ] + ] + } + ] + }); +}); +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: LambdaToDynamoDB = deployNewFunc(stack); + + expect(construct.lambdaFunction()).toBeInstanceOf(lambda.Function); + expect(construct.dynamoTable()).toBeInstanceOf(dynamodb.Table); +}); +test('check exception for Missing existingObj from props for deploy = false', () => { + const stack = new cdk.Stack(); + + const props: LambdaToDynamoDBProps = { + deployLambda: true + }; + + try { + new LambdaToDynamoDB(stack, 'test-iot-lambda-integration', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); +test('check deploy = true and no prop', () => { + const stack = new cdk.Stack(); + + const props: LambdaToDynamoDBProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + } + }; + new LambdaToDynamoDB(stack, 'test-iot-lambda-stack', props); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + Runtime: "nodejs10.x", + Environment: { + Variables: { + AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1", + DDB_TABLE_NAME: { + Ref: "testiotlambdastackDynamoTable76858356" + } + } + } + }); +}); diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/lambda/index.js new file mode 100644 index 000000000..743e4fdbb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-dynamodb/test/lambda/index.js @@ -0,0 +1,8 @@ +exports.handler = async function(event) { + console.log('request:', JSON.stringify(event, undefined, 2)); + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: `Hello, CDK! You've hit ${event.path}\n` + }; + }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/README.md b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/README.md new file mode 100644 index 000000000..5ebec61d8 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/README.md @@ -0,0 +1,84 @@ +# aws-lambda-elasticsearch-kibana module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-lambda-elasticsearch-kibana/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_lambda_elasticsearch_kibana`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana`| + +This AWS Solutions Konstruk implements the AWS Lambda function and Amazon Elasticsearch Service with the least privileged permissions. + +Here is a minimal deployable pattern definition: + +``` javascript +const { LambdaToElasticSearchAndKibana } = require('@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana'); + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' +}; + +new LambdaToElasticSearchAndKibana(stack, 'test-lambda-elasticsearch-kibana', { + lambdaFunctionProps: lambdaProps, + deployLambda: true, + domainName: 'test-domain' +}); + +``` + +## Initializer + +``` text +new LambdaToElasticSearchAndKibana(scope: Construct, id: string, props: LambdaToElasticSearchAndKibanaProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`LambdaToElasticSearchAndKibanaProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Existing instance of Lambda Function object| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user provided props to override the default props for Lambda function| +|esDomainProps?|[`elasticsearch.CfnDomainProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-elasticsearch.CfnDomainProps.html)|Optional user provided props to override the default props for the Elasticsearch Service| +|domainName|`string`|Domain name for the Cognito and the Elasticsearch Service| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Retruns an instance of lambda.Function created by the construct| +|userPool()|[`cognito.UserPool`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cognito.UserPool.html)|Retruns an instance of cognito.UserPool created by the construct| +|userPoolClient()|[`cognito.UserPoolClient`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cognito.UserPoolClient.html)|Retruns an instance of cognito.UserPoolClient created by the construct| +|identityPool()|[`cognito.CfnIdentityPool`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cognito.CfnIdentityPool.html)|Retruns an instance of cognito.CfnIdentityPool created by the construct| +|elasticsearchDomain()|[`elasticsearch.CfnDomain`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-elasticsearch.CfnDomain.html)|Retruns an instance of elasticsearch.CfnDomain created by the construct| +|cloudwatchAlarms()|[`cloudwatch.Alarm[]`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudwatch.Alarm.html)|Retruns a list of cloudwatch.Alarm created by the construct| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/architecture.png new file mode 100644 index 000000000..23ea48059 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/lib/index.ts new file mode 100644 index 000000000..7fe41e234 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/lib/index.ts @@ -0,0 +1,171 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as elasticsearch from '@aws-cdk/aws-elasticsearch'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cognito from '@aws-cdk/aws-cognito'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; +import { Role } from '@aws-cdk/aws-iam'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; + +/** + * @summary The properties for the CognitoToApiGatewayToLambda Construct + */ +export interface LambdaToElasticSearchAndKibanaProps { + /** + * Whether to create a new Lambda function or use an existing Lambda function. + * If set to false, you must provide a lambda function object as `existingLambdaObj` + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided props to override the default props for the Lambda function. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly lambdaFunctionProps?: lambda.FunctionProps + /** + * Optional user provided props to override the default props for the Elasticsearch Service. + * + * @default - Default props are used + */ + readonly esDomainProps?: elasticsearch.CfnDomainProps, + /** + * Cognito & ES Domain Name + * + * @default - None + */ + readonly domainName: string +} + +export class LambdaToElasticSearchAndKibana extends Construct { + private userpool: cognito.UserPool; + private identitypool: cognito.CfnIdentityPool; + private userpoolclient: cognito.UserPoolClient; + private elasticsearch: elasticsearch.CfnDomain; + private fn: lambda.Function; + private cwAlarms: cloudwatch.Alarm[]; + + /** + * @summary Constructs a new instance of the CognitoToApiGatewayToLambda class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {CognitoToApiGatewayToLambdaProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: LambdaToElasticSearchAndKibanaProps) { + super(scope, id); + + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + + // Find the lambda service Role ARN + const lambdaFunctionRoleARN = this.fn.role?.roleArn; + + this.userpool = defaults.buildUserPool(scope); + this.userpoolclient = defaults.buildUserPoolClient(scope, this.userpool); + this.identitypool = defaults.buildIdentityPool(scope, this.userpool, this.userpoolclient); + + const cognitoAuthorizedRole: Role = defaults.setupCognitoForElasticSearch(scope, props.domainName, { + userpool: this.userpool, + identitypool: this.identitypool, + userpoolclient: this.userpoolclient + }); + + this.elasticsearch = defaults.buildElasticSearch(scope, props.domainName, { + userpool: this.userpool, + identitypool: this.identitypool, + cognitoAuthorizedRoleARN: cognitoAuthorizedRole.roleArn, + serviceRoleARN: lambdaFunctionRoleARN}, props.esDomainProps); + + // Add ES Domain to lambda envrionment variable + this.fn.addEnvironment('DOMAIN_ENDPOINT', this.elasticsearch.attrDomainEndpoint); + + // Deploy best practices CW Alarms for ES + this.cwAlarms = defaults.buildElasticSearchCWAlarms(scope); + } + + /** + * @summary Retruns an instance of lambda.Function created by the construct. + * @returns {lambda.Function} Instance of Function created by the construct + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } + + /** + * @summary Retruns an instance of cognito.UserPool created by the construct. + * @returns {cognito.UserPool} Instance of UserPool created by the construct + * @since 0.8.0 + * @access public + */ + public userPool(): cognito.UserPool { + return this.userpool; + } + + /** + * @summary Retruns an instance of cognito.UserPoolClient created by the construct. + * @returns {cognito.UserPoolClient} Instance of UserPoolClient created by the construct + * @since 0.8.0 + * @access public + */ + public userPoolClient(): cognito.UserPoolClient { + return this.userpoolclient; + } + + /** + * @summary Retruns an instance of cognito.CfnIdentityPool created by the construct. + * @returns {cognito.CfnIdentityPool} Instance of CfnIdentityPool created by the construct + * @since 0.8.0 + * @access public + */ + public identityPool(): cognito.CfnIdentityPool { + return this.identitypool; + } + + /** + * @summary Retruns an instance of elasticsearch.CfnDomain created by the construct. + * @returns {elasticsearch.CfnDomain} Instance of CfnDomain created by the construct + * @since 0.8.0 + * @access public + */ + public elasticsearchDomain(): elasticsearch.CfnDomain { + return this.elasticsearch; + } + + /** + * @summary Retruns a list of cloudwatch.Alarm created by the construct. + * @returns {cloudwatch.Alarm[]} List of cloudwatch.Alarm created by the construct + * @since 0.8.0 + * @access public + */ + public cloudwatchAlarms(): cloudwatch.Alarm[] { + return this.cwAlarms; + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/package.json b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/package.json new file mode 100644 index 000000000..3ac3db569 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/package.json @@ -0,0 +1,83 @@ +{ + "name": "@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana", + "version": "0.8.0", + "description": "CDK Constructs for AWS Lambda to AWS Elasticsearch with Kibana integration", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.lambdaelasticsearchkibana", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "lambdaelasticsearchkibana" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.LambdaElasticsearchKibana", + "packageId": "Amazon.Konstruk.AWS.LambdaElasticsearchKibana", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-lambda-elasticsearch-kibana", + "module": "aws_solutions_konstruk.aws_lambda_elasticsearch_kibana" + } + } + }, + "dependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-cognito": "~1.25.0", + "@aws-cdk/aws-elasticsearch": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-cloudwatch": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-cognito": "~1.25.0", + "@aws-cdk/aws-elasticsearch": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-cloudwatch": "~1.25.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/__snapshots__/lambda-elasticsearch-kibana.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/__snapshots__/lambda-elasticsearch-kibana.test.js.snap new file mode 100644 index 000000000..9380edc79 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/__snapshots__/lambda-elasticsearch-kibana.test.js.snap @@ -0,0 +1,590 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test default params 1`] = ` +Object { + "Parameters": Object { + "AssetParameters67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682dbArtifactHash322F5E2F": Object { + "Description": "Artifact hash for asset \\"67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682db\\"", + "Type": "String", + }, + "AssetParameters67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682dbS3BucketBAF5BF3A": Object { + "Description": "S3 bucket for asset \\"67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682db\\"", + "Type": "String", + }, + "AssetParameters67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682dbS3VersionKeyADB3CCA3": Object { + "Description": "S3 key for asset version \\"67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682db\\"", + "Type": "String", + }, + }, + "Resources": Object { + "AutomatedSnapshotFailureTooHighAlarmA7918D4F": Object { + "Properties": Object { + "AlarmDescription": "An automated snapshot failed. This failure is often the result of a red cluster health status.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "AutomatedSnapshotFailure", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "CPUUtilizationTooHighAlarmA395C469": Object { + "Properties": Object { + "AlarmDescription": "100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "MetricName": "CPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "CognitoAuthorizedRole14E74FE0": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": Object { + "ForAnyValue:StringLike": Object { + "cognito-identity.amazonaws.com:amr": "authenticated", + }, + "StringEquals": Object { + "cognito-identity.amazonaws.com:aud": Object { + "Ref": "CognitoIdentityPool", + }, + }, + }, + "Effect": "Allow", + "Principal": Object { + "Federated": "cognito-identity.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:es:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":domain/test-domain/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CognitoAccessPolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "CognitoIdentityPool": Object { + "Properties": Object { + "AllowUnauthenticatedIdentities": false, + "CognitoIdentityProviders": Array [ + Object { + "ClientId": Object { + "Ref": "CognitoUserPoolClient5AB59AE4", + }, + "ProviderName": Object { + "Fn::GetAtt": Array [ + "CognitoUserPool53E37E69", + "ProviderName", + ], + }, + "ServerSideTokenCheck": true, + }, + ], + }, + "Type": "AWS::Cognito::IdentityPool", + }, + "CognitoKibanaConfigureRole62CCE76A": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "es.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "CognitoKibanaConfigureRolePolicy76F46A5E": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateElasticsearchDomainConfig", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "CognitoUserPool53E37E69", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:cognito-identity:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":identitypool/", + Object { + "Ref": "CognitoIdentityPool", + }, + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:es:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":domain/test-domain", + ], + ], + }, + ], + }, + Object { + "Action": "iam:PassRole", + "Condition": Object { + "StringLike": Object { + "iam:PassedToService": "cognito-identity.amazonaws.com", + }, + }, + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CognitoKibanaConfigureRolePolicy76F46A5E", + "Roles": Array [ + Object { + "Ref": "CognitoKibanaConfigureRole62CCE76A", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "CognitoUserPool53E37E69": Object { + "Properties": Object { + "LambdaConfig": Object {}, + "UserPoolAddOns": Object { + "AdvancedSecurityMode": "ENFORCED", + }, + }, + "Type": "AWS::Cognito::UserPool", + }, + "CognitoUserPoolClient5AB59AE4": Object { + "Properties": Object { + "UserPoolId": Object { + "Ref": "CognitoUserPool53E37E69", + }, + }, + "Type": "AWS::Cognito::UserPoolClient", + }, + "ElasticsearchDomain": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W28", + "reason": "The ES Domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific ES instance only", + }, + ], + }, + }, + "Properties": Object { + "AccessPolicies": Object { + "Statement": Array [ + Object { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Principal": Object { + "AWS": Array [ + Object { + "Fn::GetAtt": Array [ + "CognitoAuthorizedRole14E74FE0", + "Arn", + ], + }, + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + ], + }, + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:es:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":domain/test-domain/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "CognitoOptions": Object { + "Enabled": true, + "IdentityPoolId": Object { + "Ref": "CognitoIdentityPool", + }, + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn", + ], + }, + "UserPoolId": Object { + "Ref": "CognitoUserPool53E37E69", + }, + }, + "DomainName": "test-domain", + "EBSOptions": Object { + "EBSEnabled": true, + "VolumeSize": 10, + }, + "ElasticsearchClusterConfig": Object { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "InstanceCount": 3, + "ZoneAwarenessConfig": Object { + "AvailabilityZoneCount": 3, + }, + "ZoneAwarenessEnabled": true, + }, + "ElasticsearchVersion": "6.3", + "EncryptionAtRestOptions": Object { + "Enabled": true, + }, + "NodeToNodeEncryptionOptions": Object { + "Enabled": true, + }, + "SnapshotOptions": Object { + "AutomatedSnapshotStartHour": 1, + }, + }, + "Type": "AWS::Elasticsearch::Domain", + }, + "FreeStorageSpaceTooLowAlarm3410CBE2": Object { + "Properties": Object { + "AlarmDescription": "A node in your cluster is down to 20 GiB of free storage space.", + "ComparisonOperator": "LessThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "FreeStorageSpace", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Minimum", + "Threshold": 2000, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "IdentityPoolRoleMapping": Object { + "Properties": Object { + "IdentityPoolId": Object { + "Ref": "CognitoIdentityPool", + }, + "Roles": Object { + "authenticated": Object { + "Fn::GetAtt": Array [ + "CognitoAuthorizedRole14E74FE0", + "Arn", + ], + }, + }, + }, + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + }, + "IndexWritesBlockedTooHighAlarm5F7E9A55": Object { + "Properties": Object { + "AlarmDescription": "Your cluster is blocking write requests.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "ClusterIndexWritesBlocked", + "Namespace": "AWS/ES", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "JVMMemoryPressureTooHighAlarm303EEA7C": Object { + "Properties": Object { + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "JVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682dbS3BucketBAF5BF3A", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682dbS3VersionKeyADB3CCA3", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682dbS3VersionKeyADB3CCA3", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DOMAIN_ENDPOINT": Object { + "Fn::GetAtt": Array [ + "ElasticsearchDomain", + "DomainEndpoint", + ], + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "MasterCPUUtilizationTooHighAlarm1CE1084B": Object { + "Properties": Object { + "AlarmDescription": "Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "MetricName": "MasterCPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "MasterJVMMemoryPressureTooHighAlarmBB15F770": Object { + "Properties": Object { + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "MasterJVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "StatusRedAlarm4CE918C2": Object { + "Properties": Object { + "AlarmDescription": "At least one primary shard and its replicas are not allocated to a node. ", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "ClusterStatus.red", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "StatusYellowAlarm2B20F083": Object { + "Properties": Object { + "AlarmDescription": "At least one replica shard is not allocated to a node.", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "ClusterStatus.yellow", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "UserPoolDomain": Object { + "DependsOn": Array [ + "CognitoUserPool53E37E69", + ], + "Properties": Object { + "Domain": "test-domain", + "UserPoolId": Object { + "Ref": "CognitoUserPool53E37E69", + }, + }, + "Type": "AWS::Cognito::UserPoolDomain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..63c0bae3e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/integ.no-arguments.expected.json @@ -0,0 +1,586 @@ +{ + "Resources": { + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682dbS3BucketBAF5BF3A" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682dbS3VersionKeyADB3CCA3" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682dbS3VersionKeyADB3CCA3" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DOMAIN_ENDPOINT": { + "Fn::GetAtt": [ + "ElasticsearchDomain", + "DomainEndpoint" + ] + } + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "CognitoUserPool53E37E69": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "LambdaConfig": {}, + "UserPoolAddOns": { + "AdvancedSecurityMode": "ENFORCED" + } + } + }, + "CognitoUserPoolClient5AB59AE4": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "CognitoUserPool53E37E69" + } + } + }, + "CognitoIdentityPool": { + "Type": "AWS::Cognito::IdentityPool", + "Properties": { + "AllowUnauthenticatedIdentities": false, + "CognitoIdentityProviders": [ + { + "ClientId": { + "Ref": "CognitoUserPoolClient5AB59AE4" + }, + "ProviderName": { + "Fn::GetAtt": [ + "CognitoUserPool53E37E69", + "ProviderName" + ] + }, + "ServerSideTokenCheck": true + } + ] + } + }, + "UserPoolDomain": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "test-domain1", + "UserPoolId": { + "Ref": "CognitoUserPool53E37E69" + } + }, + "DependsOn": [ + "CognitoUserPool53E37E69" + ] + }, + "CognitoAuthorizedRole14E74FE0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "CognitoIdentityPool" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "authenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/test-domain1/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CognitoAccessPolicy" + } + ] + } + }, + "IdentityPoolRoleMapping": { + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + "Properties": { + "IdentityPoolId": { + "Ref": "CognitoIdentityPool" + }, + "Roles": { + "authenticated": { + "Fn::GetAtt": [ + "CognitoAuthorizedRole14E74FE0", + "Arn" + ] + } + } + } + }, + "CognitoKibanaConfigureRole62CCE76A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "es.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CognitoKibanaConfigureRolePolicy76F46A5E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateElasticsearchDomainConfig" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "CognitoUserPool53E37E69", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:cognito-identity:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":identitypool/", + { + "Ref": "CognitoIdentityPool" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/test-domain1" + ] + ] + } + ] + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringLike": { + "iam:PassedToService": "cognito-identity.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CognitoKibanaConfigureRolePolicy76F46A5E", + "Roles": [ + { + "Ref": "CognitoKibanaConfigureRole62CCE76A" + } + ] + } + }, + "ElasticsearchDomain": { + "Type": "AWS::Elasticsearch::Domain", + "Properties": { + "AccessPolicies": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Principal": { + "AWS": [ + { + "Fn::GetAtt": [ + "CognitoAuthorizedRole14E74FE0", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + } + ] + }, + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/test-domain1/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "CognitoOptions": { + "Enabled": true, + "IdentityPoolId": { + "Ref": "CognitoIdentityPool" + }, + "RoleArn": { + "Fn::GetAtt": [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn" + ] + }, + "UserPoolId": { + "Ref": "CognitoUserPool53E37E69" + } + }, + "DomainName": "test-domain1", + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10 + }, + "ElasticsearchClusterConfig": { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "InstanceCount": 3, + "ZoneAwarenessConfig": { + "AvailabilityZoneCount": 3 + }, + "ZoneAwarenessEnabled": true + }, + "ElasticsearchVersion": "6.3", + "EncryptionAtRestOptions": { + "Enabled": true + }, + "NodeToNodeEncryptionOptions": { + "Enabled": true + }, + "SnapshotOptions": { + "AutomatedSnapshotStartHour": 1 + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W28", + "reason": "The ES Domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific ES instance only" + } + ] + } + } + }, + "StatusRedAlarm4CE918C2": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one primary shard and its replicas are not allocated to a node. ", + "MetricName": "ClusterStatus.red", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "StatusYellowAlarm2B20F083": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one replica shard is not allocated to a node.", + "MetricName": "ClusterStatus.yellow", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "FreeStorageSpaceTooLowAlarm3410CBE2": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "LessThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "A node in your cluster is down to 20 GiB of free storage space.", + "MetricName": "FreeStorageSpace", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Minimum", + "Threshold": 2000 + } + }, + "IndexWritesBlockedTooHighAlarm5F7E9A55": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Your cluster is blocking write requests.", + "MetricName": "ClusterIndexWritesBlocked", + "Namespace": "AWS/ES", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "AutomatedSnapshotFailureTooHighAlarmA7918D4F": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "An automated snapshot failed. This failure is often the result of a red cluster health status.", + "MetricName": "AutomatedSnapshotFailure", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "CPUUtilizationTooHighAlarmA395C469": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.", + "MetricName": "CPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "JVMMemoryPressureTooHighAlarm303EEA7C": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "JVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "MasterCPUUtilizationTooHighAlarm1CE1084B": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.", + "MetricName": "MasterCPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "MasterJVMMemoryPressureTooHighAlarmBB15F770": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "MasterJVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + } + }, + "Parameters": { + "AssetParameters67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682dbS3BucketBAF5BF3A": { + "Type": "String", + "Description": "S3 bucket for asset \"67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682db\"" + }, + "AssetParameters67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682dbS3VersionKeyADB3CCA3": { + "Type": "String", + "Description": "S3 key for asset version \"67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682db\"" + }, + "AssetParameters67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682dbArtifactHash322F5E2F": { + "Type": "String", + "Description": "Artifact hash for asset \"67a9971e29baab2bde3043bb70ce5b53318b95429a1ce9b189cf65223e8682db\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/integ.no-arguments.ts new file mode 100644 index 000000000..77a79c83a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/integ.no-arguments.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { LambdaToElasticSearchAndKibana } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-lambda-elasticsearch-kibana-stack'); + +const lambdaProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' +}; + +new LambdaToElasticSearchAndKibana(stack, 'test-lambda-elasticsearch-kibana', { + lambdaFunctionProps: lambdaProps, + deployLambda: true, + domainName: 'test-domain1' +}); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/lambda-elasticsearch-kibana.test.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/lambda-elasticsearch-kibana.test.ts new file mode 100644 index 000000000..62ad18f0c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/lambda-elasticsearch-kibana.test.ts @@ -0,0 +1,86 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { LambdaToElasticSearchAndKibana, LambdaToElasticSearchAndKibanaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from "@aws-cdk/core"; +import '@aws-cdk/assert/jest'; +import { CfnDomain } from '@aws-cdk/aws-elasticsearch'; +import { CfnIdentityPool, UserPool, UserPoolClient } from '@aws-cdk/aws-cognito'; + +function deployNewFunc(stack: cdk.Stack) { + const props: LambdaToElasticSearchAndKibanaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }, + domainName: 'test-domain' + }; + + return new LambdaToElasticSearchAndKibana(stack, 'test-lambda-elasticsearch-stack', props); +} + +test('snapshot test default params', () => { + const stack = new cdk.Stack(); + deployNewFunc(stack); + + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check domain names', () => { + const stack = new cdk.Stack(); + + deployNewFunc(stack); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolDomain', { + Domain: "test-domain", + UserPoolId: { + Ref: "CognitoUserPool53E37E69" + } + }); + + expect(stack).toHaveResource('AWS::Elasticsearch::Domain', { + DomainName: "test-domain", + }); +}); + +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: LambdaToElasticSearchAndKibana = deployNewFunc(stack); + + expect(construct.lambdaFunction()).toBeInstanceOf(lambda.Function); + expect(construct.elasticsearchDomain()).toBeInstanceOf(CfnDomain); + expect(construct.identityPool()).toBeInstanceOf(CfnIdentityPool); + expect(construct.userPool()).toBeInstanceOf(UserPool); + expect(construct.userPoolClient()).toBeInstanceOf(UserPoolClient); + expect(construct.cloudwatchAlarms()).toHaveLength(9); +}); + +test('check exception for Missing existingObj from props for deploy = false', () => { + const stack = new cdk.Stack(); + + const props: LambdaToElasticSearchAndKibanaProps = { + deployLambda: true, + domainName: 'test-domain' + }; + + try { + new LambdaToElasticSearchAndKibana(stack, 'test-lambda-elasticsearch-stack', props); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/lambda/index.js new file mode 100644 index 000000000..7a660ab80 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-elasticsearch-kibana/test/lambda/index.js @@ -0,0 +1,56 @@ +var AWS = require('aws-sdk'); +var path = require('path'); + +console.log('Loading function'); + +var esDomain = { + endpoint: process.env.DOMAIN_ENDPOINT, + region: process.env.AWS_REGION, + index: 'records', + doctype: 'movie' +}; + +var creds = new AWS.EnvironmentCredentials('AWS'); +var endpoint = new AWS.Endpoint(esDomain.endpoint); + +function postDocumentToES(doc, context) { + var req = new AWS.HttpRequest(endpoint); + + req.method = 'POST'; + req.path = path.join('/', esDomain.index, esDomain.doctype); + req.region = esDomain.region; + req.body = doc; + req.headers['presigned-expires'] = false; + req.headers['Host'] = esDomain.endpoint; + req.headers['Content-Type'] = 'application/json'; + + // Sign the request (Sigv4) + var signer = new AWS.Signers.V4(req, 'es'); + signer.addAuthorization(creds, new Date()); + + // Post document to ES + var send = new AWS.NodeHttpClient(); + send.handleRequest(req, null, function(httpResp) { + var body = ''; + httpResp.on('data', function (chunk) { + body += chunk; + }); + httpResp.on('end', function (chunk) { + console.log('All movie records added to ES.'); + context.succeed(); + }); + }, function(err) { + console.log('Error: ' + err); + context.fail(); + }); +} + +exports.handler = (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); + postDocumentToES("{ \"title\": \"Spirited Away\" }", context); + return { +      statusCode: 200, +      headers: { 'Content-Type': 'text/plain' }, +      body: `Hello from Project Vesper! You've hit ${event.path}\n` +    }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/README.md b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/README.md new file mode 100644 index 000000000..219997e03 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/README.md @@ -0,0 +1,76 @@ +# aws-lambda-s3 module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-lambda-s3/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_lambda_s3`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-lambda-s3`| + +This AWS Solutions Konstruk implements an AWS Lambda function connected to an Amazon S3 bucket. + +Here is a minimal deployable pattern definition: + +``` javascript +const { LambdaToS3 } = require('@aws-solutions-konstruk/aws-lambda-s3'); + +new LambdaToS3(stack, 'LambdaToS3Pattern', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } +}); + +``` + +## Initializer + +``` text +new LambdaToS3(scope: Construct, id: string, props: LambdaToS3Props); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`LambdaToS3Props`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function.| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|An optional, existing Lambda function.| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user-provided props to override the default props for the Lambda function.| +|deployBucket?|`boolean`|Whether to create a S3 Bucket or use an existing S3 Bucket| +|existingBucketObj?|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Existing instance of S3 Bucket object| +|bucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|Optional user provided props to override the default props for S3 Bucket| +|bucketPermissions?|`string[]`|Optional bucket permissions to grant to the Lambda function. One or more of the following may be specified: `Delete`, `Put`, `Read`, `ReadWrite`, `Write`.| + + +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Returns an instance of the Lambda function created by the pattern.| +|s3Bucket()|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Returns an instance of the S3 bucket created by the pattern.| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/architecture.png new file mode 100644 index 000000000..b80d4b14f Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/lib/index.ts new file mode 100644 index 000000000..a3154d45c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/lib/index.ts @@ -0,0 +1,163 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import * as lambda from '@aws-cdk/aws-lambda'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; + +/** + * @summary The properties for the LambdaToS3 class. + */ +export interface LambdaToS3Props { + /** + * Whether to create a new Lambda function or use an existing Lambda function. + * If set to false, you must provide an existing function for the `existingLambdaObj` property. + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided properties to override the default properties for the Lambda function. + * If `deploy` is set to true only then this property is required. + * + * @default - Default properties are used. + */ + readonly lambdaFunctionProps?: lambda.FunctionProps | any + /** + * Whether to create a S3 Bucket or use an existing S3 Bucket. + * If set to false, you must provide S3 Bucket as `existingBucketObj` + * + * @default - true + */ + readonly deployBucket?: boolean, + /** + * Existing instance of S3 Bucket object. + * If `deployBucket` is set to false only then this property is required + * + * @default - None + */ + readonly existingBucketObj?: s3.Bucket, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly bucketProps?: s3.BucketProps + /** + * Optional bucket permissions to grant to the Lambda function. + * One or more of the following may be specified: "Delete", "Put", "Read", "ReadWrite", "Write". + * + * @default - Read/write access is given to the Lambda function if no value is specified. + */ + readonly bucketPermissions?: string[] +} + +/** + * @summary The LambdaToS3 class. + */ +export class LambdaToS3 extends Construct { + // Private variables + private fn: lambda.Function; + private bucket: s3.Bucket; + + /** + * @summary Constructs a new instance of the LambdaToS3 class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {LambdaToS3Props} props - user provided props for the construct. + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: LambdaToS3Props) { + super(scope, id); + + // Setup the Lambda function + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + + // Setup the S3 bucket + this.bucket = defaults.buildS3Bucket(this, { + deployBucket: props.deployBucket, + existingBucketObj: props.existingBucketObj, + bucketProps: props.bucketProps + }); + + // Configure environment variables + this.fn.addEnvironment('S3_BUCKET_NAME', this.bucket.bucketName); + + // Add the requested or default bucket permissions + if (props.hasOwnProperty('bucketPermissions') && props.bucketPermissions) { + if (props.bucketPermissions.includes('Delete')) { + this.bucket.grantDelete(this.fn.grantPrincipal); + } + if (props.bucketPermissions.includes('Put')) { + this.bucket.grantPut(this.fn.grantPrincipal); + } + if (props.bucketPermissions.includes('Read')) { + this.bucket.grantRead(this.fn.grantPrincipal); + } + if (props.bucketPermissions.includes('ReadWrite')) { + this.bucket.grantReadWrite(this.fn.grantPrincipal); + } + if (props.bucketPermissions.includes('Write')) { + this.bucket.grantWrite(this.fn.grantPrincipal); + } + } else { + this.bucket.grantReadWrite(this.fn.grantPrincipal); + } + + // Add appropriate metadata + const s3BucketResource = this.bucket.node.findChild('Resource') as s3.CfnBucket; + s3BucketResource.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W51', + reason: `This S3 bucket Bucket does not need a bucket policy` + }] + } + }; + } + + /** + * @summary Returns an instance of the lambda.Function created by the construct. + * @returns {lambda.Function} Instance of the Function created by the construct. + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } + + /** + * @summary Returns an instance of the s3.Bucket created by the construct. + * @returns {s3.Bucket} Instance of the Bucket created by the construct. + * @since 0.8.0 + * @access public + */ + public s3Bucket(): s3.Bucket { + return this.bucket; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/package.json b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/package.json new file mode 100644 index 000000000..b920dad95 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/package.json @@ -0,0 +1,76 @@ +{ + "name": "@aws-solutions-konstruk/aws-lambda-s3", + "version": "0.8.0", + "description": "CDK constructs for defining an interaction between an AWS Lambda function and an Amazon S3 bucket.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-lambda-s3" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.lambdas3", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "lambdas3" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.LambdaS3", + "packageId": "Amazon.Konstruk.AWS.LambdaS3", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-lambda-s3", + "module": "aws_solutions_konstruk.aws_lambda_s3" + } + } + }, + "dependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/__snapshots__/lambda-s3.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/__snapshots__/lambda-s3.test.js.snap new file mode 100644 index 000000000..95fb268b7 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/__snapshots__/lambda-s3.test.js.snap @@ -0,0 +1,2178 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test deployment w/ s3 multiple permissions 1`] = ` +Object { + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "S3_BUCKET_NAME": Object { + "Ref": "lambdatos3stackS3BucketB9FD9B29", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:DeleteObject*", + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + Object { + "Action": Array [ + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdatos3stackS3BucketB9FD9B29": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "lambdatos3stackS3LoggingBucketB82C3492", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "lambdatos3stackS3LoggingBucketB82C3492": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; + +exports[`Test deployment w/ s3:Delete only 1`] = ` +Object { + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "S3_BUCKET_NAME": Object { + "Ref": "lambdatos3stackS3BucketB9FD9B29", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:DeleteObject*", + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdatos3stackS3BucketB9FD9B29": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "lambdatos3stackS3LoggingBucketB82C3492", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "lambdatos3stackS3LoggingBucketB82C3492": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; + +exports[`Test deployment w/ s3:Put only 1`] = ` +Object { + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "S3_BUCKET_NAME": Object { + "Ref": "lambdatos3stackS3BucketB9FD9B29", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:PutObject*", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdatos3stackS3BucketB9FD9B29": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "lambdatos3stackS3LoggingBucketB82C3492", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "lambdatos3stackS3LoggingBucketB82C3492": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; + +exports[`Test deployment w/ s3:Read only 1`] = ` +Object { + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "S3_BUCKET_NAME": Object { + "Ref": "lambdatos3stackS3BucketB9FD9B29", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdatos3stackS3BucketB9FD9B29": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "lambdatos3stackS3LoggingBucketB82C3492", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "lambdatos3stackS3LoggingBucketB82C3492": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; + +exports[`Test deployment w/ s3:ReadWrite only 1`] = ` +Object { + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "S3_BUCKET_NAME": Object { + "Ref": "lambdatos3stackS3BucketB9FD9B29", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdatos3stackS3BucketB9FD9B29": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "lambdatos3stackS3LoggingBucketB82C3492", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "lambdatos3stackS3LoggingBucketB82C3492": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; + +exports[`Test deployment w/ s3:Write only 1`] = ` +Object { + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "S3_BUCKET_NAME": Object { + "Ref": "lambdatos3stackS3BucketB9FD9B29", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdatos3stackS3BucketB9FD9B29": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "lambdatos3stackS3LoggingBucketB82C3492", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "lambdatos3stackS3LoggingBucketB82C3492": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; + +exports[`Test minimal deployment with new Lambda function 1`] = ` +Object { + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "S3_BUCKET_NAME": Object { + "Ref": "lambdatos3stackS3BucketB9FD9B29", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdatos3stackS3BucketB9FD9B29": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "lambdatos3stackS3LoggingBucketB82C3492", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "lambdatos3stackS3LoggingBucketB82C3492": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; + +exports[`Test the bucketProps override 1`] = ` +Object { + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "S3_BUCKET_NAME": Object { + "Ref": "lambdatos3stackS3BucketB9FD9B29", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "lambdatos3stackS3BucketB9FD9B29", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdatos3stackS3BucketB9FD9B29": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "lambdatos3stackS3LoggingBucketB82C3492", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + "WebsiteConfiguration": Object { + "IndexDocument": "index.main.html", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "lambdatos3stackS3LoggingBucketB82C3492": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/integ.deployFunction.expected.json b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/integ.deployFunction.expected.json new file mode 100644 index 000000000..644e9e3c0 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/integ.deployFunction.expected.json @@ -0,0 +1,272 @@ +{ + "Description": "Integration Test for aws-lambda-s3", + "Resources": { + "testlambdas3S3LoggingBucketD42FC73D": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + }, + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "testlambdas3S3Bucket179A52E6": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "testlambdas3S3LoggingBucketD42FC73D" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlambdas3S3Bucket179A52E6", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testlambdas3S3Bucket179A52E6", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "S3_BUCKET_NAME": { + "Ref": "testlambdas3S3Bucket179A52E6" + } + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + } + }, + "Parameters": { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": { + "Type": "String", + "Description": "S3 bucket for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": { + "Type": "String", + "Description": "S3 key for asset version \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": { + "Type": "String", + "Description": "Artifact hash for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/integ.deployFunction.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/integ.deployFunction.ts new file mode 100644 index 000000000..21768b370 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/integ.deployFunction.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { LambdaToS3, LambdaToS3Props } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-lambda-s3'); +stack.templateOptions.description = 'Integration Test for aws-lambda-s3'; + +// Definitions +const props: LambdaToS3Props = { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } +}; + +new LambdaToS3(stack, 'test-lambda-s3', props); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/integ.existingFunction.expected.json b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/integ.existingFunction.expected.json new file mode 100644 index 000000000..6564857ba --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/integ.existingFunction.expected.json @@ -0,0 +1,272 @@ +{ + "Description": "Integration Test for aws-lambda-s3", + "Resources": { + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlambdas3S3Bucket179A52E6", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testlambdas3S3Bucket179A52E6", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "S3_BUCKET_NAME": { + "Ref": "testlambdas3S3Bucket179A52E6" + } + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "testlambdas3S3LoggingBucketD42FC73D": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + }, + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "testlambdas3S3Bucket179A52E6": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "testlambdas3S3LoggingBucketD42FC73D" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + } + }, + "Parameters": { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": { + "Type": "String", + "Description": "S3 bucket for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": { + "Type": "String", + "Description": "S3 key for asset version \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": { + "Type": "String", + "Description": "Artifact hash for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/integ.existingFunction.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/integ.existingFunction.ts new file mode 100644 index 000000000..738079769 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/integ.existingFunction.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { LambdaToS3, LambdaToS3Props } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as defaults from '@aws-solutions-konstruk/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-lambda-s3'); +stack.templateOptions.description = 'Integration Test for aws-lambda-s3'; + +// Definitions +const lambdaFunctionProps = { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) +}; + +const func = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + +const props: LambdaToS3Props = { + deployLambda: false, + existingLambdaObj: func +}; + +new LambdaToS3(stack, 'test-lambda-s3', props); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/lambda-s3.test.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/lambda-s3.test.ts new file mode 100644 index 000000000..324d3506a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/lambda-s3.test.ts @@ -0,0 +1,210 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { Stack } from "@aws-cdk/core"; +import * as lambda from "@aws-cdk/aws-lambda"; +import { LambdaToS3 } from '../lib'; +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +// -------------------------------------------------------------- +// Test minimal deployment with new Lambda function +// -------------------------------------------------------------- +test('Test minimal deployment with new Lambda function', () => { + // Stack + const stack = new Stack(); + // Helper declaration + new LambdaToS3(stack, 'lambda-to-s3-stack', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test deployment w/ s3:Delete only +// -------------------------------------------------------------- +test('Test deployment w/ s3:Delete only', () => { + // Stack + const stack = new Stack(); + // Helper declaration + new LambdaToS3(stack, 'lambda-to-s3-stack', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + bucketPermissions: ['Delete'] + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test deployment w/ s3:Put only +// -------------------------------------------------------------- +test('Test deployment w/ s3:Put only', () => { + // Stack + const stack = new Stack(); + // Helper declaration + new LambdaToS3(stack, 'lambda-to-s3-stack', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + bucketPermissions: ['Put'] + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test deployment w/ s3:Read only +// -------------------------------------------------------------- +test('Test deployment w/ s3:Read only', () => { + // Stack + const stack = new Stack(); + // Helper declaration + new LambdaToS3(stack, 'lambda-to-s3-stack', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + bucketPermissions: ['Read'] + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test deployment w/ s3:ReadWrite only +// -------------------------------------------------------------- +test('Test deployment w/ s3:ReadWrite only', () => { + // Stack + const stack = new Stack(); + // Helper declaration + new LambdaToS3(stack, 'lambda-to-s3-stack', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + bucketPermissions: ['ReadWrite'] + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test deployment w/ s3:Write only +// -------------------------------------------------------------- +test('Test deployment w/ s3:Write only', () => { + // Stack + const stack = new Stack(); + // Helper declaration + new LambdaToS3(stack, 'lambda-to-s3-stack', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + bucketPermissions: ['Write'] + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test deployment w/ s3 multiple permissions +// -------------------------------------------------------------- +test('Test deployment w/ s3 multiple permissions', () => { + // Stack + const stack = new Stack(); + // Helper declaration + new LambdaToS3(stack, 'lambda-to-s3-stack', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + bucketPermissions: ['Write', 'Delete'] + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test the getter methods +// -------------------------------------------------------------- +test('Test the getter methods', () => { + // Stack + const stack = new Stack(); + // Helper declaration + const pattern = new LambdaToS3(stack, 'lambda-to-s3-stack', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + bucketPermissions: ['Write'] + }); + // Assertion 1 + const func = pattern.lambdaFunction(); + expect(func).toBeDefined(); + // Assertion 2 + const bucket = pattern.s3Bucket(); + expect(bucket).toBeDefined(); +}); + +// -------------------------------------------------------------- +// Test the bucketProps override +// -------------------------------------------------------------- +test('Test the bucketProps override', () => { + // Stack + const stack = new Stack(); + // Helper declaration + new LambdaToS3(stack, 'lambda-to-s3-stack', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + bucketProps: { + websiteIndexDocument: 'index.main.html' + } + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // Assertion 2 + expect(stack).toHaveResource("AWS::S3::Bucket", { + WebsiteConfiguration: { + IndexDocument: 'index.main.html' + } + }); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/lambda/index.js new file mode 100644 index 000000000..51fdc6953 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-s3/test/lambda/index.js @@ -0,0 +1,21 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +exports.handler = async (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); +    return { +      statusCode: 200, +      headers: { 'Content-Type': 'text/plain' }, +      body: `Hello from Project Vesper! You've hit ${event.path}\n` +    }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/README.md b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/README.md new file mode 100644 index 000000000..bd54833d6 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/README.md @@ -0,0 +1,78 @@ +# aws-lambda-sns module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-lambda-sns/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_lambda_sns`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-lambda-sns`| + +This AWS Solutions Konstruk implements an AWS Lambda function connected to an Amazon SNS topic. + +Here is a minimal deployable pattern definition: + +``` javascript +const { LambdaToSns } = require('@aws-solutions-konstruk/aws-lambda-sns'); + +new LambdaToSns(stack, 'LambdaToSnsPattern', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } +}); + +``` + +## Initializer + +``` text +new LambdaToSns(scope: Construct, id: string, props: LambdaToSnsProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`LambdaToSnsProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function.| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|An optional, existing Lambda function.| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user-provided props to override the default props for the Lambda function.| +|topicProps?|[`sns.TopicProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sns.TopicProps.html)|Optional user provided properties to override the default properties for the SNS topic.| +|enableEncryption?|`boolean`|Use a KMS Key, either managed by this CDK app, or imported. If importing an encryption key, it must be specified in the encryptionKey property for this construct.| +|encryptionKey?|[`kms.Key`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kms.Key.html)|An optional, imported encryption key to encrypt the SNS topic with.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Returns an instance of the Lambda function created by the pattern.| +|snsTopic()|[`sns.Topic`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sns.Topic.html)|Returns an instance of the SNS topic created by the pattern.| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/architecture.png new file mode 100644 index 000000000..b6990c4ff Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/lib/index.ts new file mode 100644 index 000000000..e4364fd4d --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/lib/index.ts @@ -0,0 +1,126 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sns from '@aws-cdk/aws-sns'; +import * as kms from '@aws-cdk/aws-kms'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; + +/** + * @summary The properties for the LambdaToSns class. + */ +export interface LambdaToSnsProps { + /** + * Whether to create a new Lambda function or use an existing Lambda function. + * If set to false, you must provide an existing function for the `existingLambdaObj` property. + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided properties to override the default properties for the Lambda function. + * If `deploy` is set to true only then this property is required. + * + * @default - Default properties are used. + */ + readonly lambdaFunctionProps?: lambda.FunctionProps | any + /** + * Optional user provided properties to override the default properties for the SNS topic. + * + * @default - Default properties are used. + */ + readonly topicProps?: sns.TopicProps | any + /** + * Use a KMS Key, either managed by this CDK app, or imported. If importing an encryption key, it must be specified in + * the encryptionKey property for this construct. + * + * @default - true (encryption enabled, managed by this CDK app). + */ + readonly enableEncryption?: boolean + /** + * An optional, imported encryption key to encrypt the SNS topic with. + * + * @default - not specified. + */ + readonly encryptionKey?: kms.Key +} + +/** + * @summary The LambdaToSns class. + */ +export class LambdaToSns extends Construct { + // Private variables + private fn: lambda.Function; + private topic: sns.Topic; + + /** + * @summary Constructs a new instance of the LambdaToSns class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {LambdaToSnsProps} props - user provided props for the construct. + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: LambdaToSnsProps) { + super(scope, id); + + // Setup the Lambda function + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + + // Setup the SNS topic + this.topic = defaults.buildTopic(this, { + enableEncryption: props.enableEncryption, + encryptionKey: props.encryptionKey + }); + + // Configure environment variables + this.fn.addEnvironment('SNS_TOPIC_NAME', this.topic.topicName); + this.fn.addEnvironment('SNS_TOPIC_ARN', this.topic.topicArn); + + // Add publishing permissions to the function + this.topic.grantPublish(this.fn.grantPrincipal); + } + + /** + * @summary Returns an instance of the lambda.Function created by the construct. + * @returns {lambda.Function} Instance of the Function created by the construct. + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } + + /** + * @summary Returns an instance of the sns.Topic created by the construct. + * @returns {sns.Topic} Instance of the Topic created by the construct. + * @since 0.8.0 + * @access public + */ + public snsTopic(): sns.Topic { + return this.topic; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/package.json b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/package.json new file mode 100644 index 000000000..4e3341de9 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/package.json @@ -0,0 +1,78 @@ +{ + "name": "@aws-solutions-konstruk/aws-lambda-sns", + "version": "0.8.0", + "description": "CDK constructs for defining an interaction between an AWS Lambda function and an Amazon SNS topic.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-lambda-sns" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.lambdasns", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "lambdasns" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.LambdaSns", + "packageId": "Amazon.Konstruk.AWS.LambdaSns", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-lambda-sns", + "module": "aws_solutions_konstruk.aws_lambda_sns" + } + } + }, + "dependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-sns": "~1.25.0", + "@aws-cdk/aws-kms": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-sns": "~1.25.0", + "@aws-cdk/aws-kms": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/__snapshots__/lambda-sns.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/__snapshots__/lambda-sns.test.js.snap new file mode 100644 index 000000000..c6788a0ef --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/__snapshots__/lambda-sns.test.js.snap @@ -0,0 +1,706 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test deployment with existing Lambda function 1`] = ` +Object { + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "LAMBDA_NAME": "override-function", + "SNS_TOPIC_ARN": Object { + "Ref": "lambdatosnsstackSnsTopic6292A14A", + }, + "SNS_TOPIC_NAME": Object { + "Fn::GetAtt": Array [ + "lambdatosnsstackSnsTopic6292A14A", + "TopicName", + ], + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": Object { + "Ref": "lambdatosnsstackSnsTopic6292A14A", + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdatosnsstackEncryptionKeyF46E3814": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "lambdatosnsstackSnsTopic6292A14A": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Ref": "lambdatosnsstackEncryptionKeyF46E3814", + }, + }, + "Type": "AWS::SNS::Topic", + }, + }, +} +`; + +exports[`Test deployment with imported encryption key 1`] = ` +Object { + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "LAMBDA_NAME": "deployed-function-no-enc", + "SNS_TOPIC_ARN": Object { + "Ref": "lambdatosnsstackSnsTopic6292A14A", + }, + "SNS_TOPIC_NAME": Object { + "Fn::GetAtt": Array [ + "lambdatosnsstackSnsTopic6292A14A", + "TopicName", + ], + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": Object { + "Ref": "lambdatosnsstackSnsTopic6292A14A", + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "importedkey38675D68": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": false, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "lambdatosnsstackSnsTopic6292A14A": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Ref": "importedkey38675D68", + }, + }, + "Type": "AWS::SNS::Topic", + }, + }, +} +`; + +exports[`Test deployment with new Lambda function 1`] = ` +Object { + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "LAMBDA_NAME": "deployed-function", + "SNS_TOPIC_ARN": Object { + "Ref": "lambdatosnsstackSnsTopic6292A14A", + }, + "SNS_TOPIC_NAME": Object { + "Fn::GetAtt": Array [ + "lambdatosnsstackSnsTopic6292A14A", + "TopicName", + ], + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": Object { + "Ref": "lambdatosnsstackSnsTopic6292A14A", + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "lambdatosnsstackEncryptionKeyF46E3814": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "lambdatosnsstackSnsTopic6292A14A": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Ref": "lambdatosnsstackEncryptionKeyF46E3814", + }, + }, + "Type": "AWS::SNS::Topic", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/integ.deployFunction.expected.json b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/integ.deployFunction.expected.json new file mode 100644 index 000000000..8894271f5 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/integ.deployFunction.expected.json @@ -0,0 +1,232 @@ +{ + "Description": "Integration Test for aws-lambda-sns", + "Resources": { + "testlambdasnsEncryptionKey57F4E220": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "EnableKeyRotation": true + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testlambdasnsSnsTopic57DFED98": { + "Type": "AWS::SNS::Topic", + "Properties": { + "KmsMasterKeyId": { + "Ref": "testlambdasnsEncryptionKey57F4E220" + } + } + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "testlambdasnsSnsTopic57DFED98" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "SNS_TOPIC_NAME": { + "Fn::GetAtt": [ + "testlambdasnsSnsTopic57DFED98", + "TopicName" + ] + }, + "SNS_TOPIC_ARN": { + "Ref": "testlambdasnsSnsTopic57DFED98" + } + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + } + }, + "Parameters": { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": { + "Type": "String", + "Description": "S3 bucket for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": { + "Type": "String", + "Description": "S3 key for asset version \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": { + "Type": "String", + "Description": "Artifact hash for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/integ.deployFunction.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/integ.deployFunction.ts new file mode 100644 index 000000000..05a303c02 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/integ.deployFunction.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { LambdaToSns, LambdaToSnsProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-lambda-sns'); +stack.templateOptions.description = 'Integration Test for aws-lambda-sns'; + +// Definitions +const props: LambdaToSnsProps = { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } +}; + +new LambdaToSns(stack, 'test-lambda-sns', props); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/integ.existingFunction.expected.json b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/integ.existingFunction.expected.json new file mode 100644 index 000000000..e6981364c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/integ.existingFunction.expected.json @@ -0,0 +1,232 @@ +{ + "Description": "Integration Test for aws-lambda-sns", + "Resources": { + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "testlambdasnsSnsTopic57DFED98" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "SNS_TOPIC_NAME": { + "Fn::GetAtt": [ + "testlambdasnsSnsTopic57DFED98", + "TopicName" + ] + }, + "SNS_TOPIC_ARN": { + "Ref": "testlambdasnsSnsTopic57DFED98" + } + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "testlambdasnsEncryptionKey57F4E220": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "EnableKeyRotation": true + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testlambdasnsSnsTopic57DFED98": { + "Type": "AWS::SNS::Topic", + "Properties": { + "KmsMasterKeyId": { + "Ref": "testlambdasnsEncryptionKey57F4E220" + } + } + } + }, + "Parameters": { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": { + "Type": "String", + "Description": "S3 bucket for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": { + "Type": "String", + "Description": "S3 key for asset version \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": { + "Type": "String", + "Description": "Artifact hash for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/integ.existingFunction.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/integ.existingFunction.ts new file mode 100644 index 000000000..575b481d7 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/integ.existingFunction.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { LambdaToSns, LambdaToSnsProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as defaults from '@aws-solutions-konstruk/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-lambda-sns'); +stack.templateOptions.description = 'Integration Test for aws-lambda-sns'; + +// Definitions +const lambdaFunctionProps = { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) +}; + +const func = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + +const props: LambdaToSnsProps = { + deployLambda: false, + existingLambdaObj: func +}; + +new LambdaToSns(stack, 'test-lambda-sns', props); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/lambda-sns.test.ts b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/lambda-sns.test.ts new file mode 100644 index 000000000..e24fb0287 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/lambda-sns.test.ts @@ -0,0 +1,151 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { Stack } from "@aws-cdk/core"; +import * as kms from "@aws-cdk/aws-kms"; +import * as lambda from "@aws-cdk/aws-lambda"; +import { LambdaToSns } from '../lib'; +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +// -------------------------------------------------------------- +// Test deployment with new Lambda function +// -------------------------------------------------------------- +test('Test deployment with new Lambda function', () => { + // Stack + const stack = new Stack(); + // Helper declaration + new LambdaToSns(stack, 'lambda-to-sns-stack', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`), + environment: { + LAMBDA_NAME: 'deployed-function' + } + } + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // Assertion 2 + expect(stack).toHaveResourceLike("AWS::Lambda::Function", { + Environment: { + Variables: { + LAMBDA_NAME: 'deployed-function' + } + } + }); + // Assertion 3 + expect(stack).toHaveResource("AWS::KMS::Key", { + EnableKeyRotation: true + }); +}); + +// -------------------------------------------------------------- +// Test deployment with existing Lambda function +// -------------------------------------------------------------- +test('Test deployment with existing Lambda function', () => { + // Stack + const stack = new Stack(); + // Helper declaration + new LambdaToSns(stack, 'lambda-to-sns-stack', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`), + environment: { + LAMBDA_NAME: 'override-function' + } + } + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // Assertion 2 + expect(stack).toHaveResourceLike("AWS::Lambda::Function", { + Environment: { + Variables: { + LAMBDA_NAME: 'override-function' + } + } + }); + // Assertion 3 + expect(stack).toHaveResource("AWS::KMS::Key", { + EnableKeyRotation: true + }); +}); + +// -------------------------------------------------------------- +// Test deployment with imported encryption key +// -------------------------------------------------------------- +test('Test deployment with imported encryption key', () => { + // Stack + const stack = new Stack(); + // Setup + const kmsKey = new kms.Key(stack, 'imported-key', { + enableKeyRotation: false + }); + // Helper declaration + new LambdaToSns(stack, 'lambda-to-sns-stack', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`), + environment: { + LAMBDA_NAME: 'deployed-function-no-enc' + } + }, + enableEncryption: true, + encryptionKey: kmsKey + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // Assertion 2 + expect(stack).toHaveResourceLike("AWS::Lambda::Function", { + Environment: { + Variables: { + LAMBDA_NAME: 'deployed-function-no-enc' + } + } + }); + // Assertion 3 + expect(stack).toHaveResource("AWS::KMS::Key", { + EnableKeyRotation: false + }); +}); + +// -------------------------------------------------------------- +// Test the getter methods +// -------------------------------------------------------------- +test('Test the getter methods', () => { + // Stack + const stack = new Stack(); + // Helper declaration + const pattern = new LambdaToSns(stack, 'lambda-to-sns-stack', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } + }); + // Assertion 1 + const func = pattern.lambdaFunction(); + expect(func).toBeDefined(); + // Assertion 2 + const topic = pattern.snsTopic(); + expect(topic).toBeDefined(); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/lambda/index.js new file mode 100644 index 000000000..51fdc6953 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-lambda-sns/test/lambda/index.js @@ -0,0 +1,21 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +exports.handler = async (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); +    return { +      statusCode: 200, +      headers: { 'Content-Type': 'text/plain' }, +      body: `Hello from Project Vesper! You've hit ${event.path}\n` +    }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/README.md b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/README.md new file mode 100644 index 000000000..188a26ea2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/README.md @@ -0,0 +1,85 @@ +# aws-s3-lambda module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-s3-lambda/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_s3_lambda`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-s3-lambda`| + +This AWS Solutions Konstruk implements an Amazon S3 bucket connected to an AWS Lambda function. + +Here is a minimal deployable pattern definition: + +``` javascript +const { S3ToLambdaProps, S3ToLambda } = require('@aws-solutions-konstruk/aws-s3-lambda'); + +const stack = new Stack(app, 'test-s3-lambda-stack'); + +const props: S3ToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, +}; + +new S3ToLambda(stack, 'test-s3-lambda', props); + +``` + +## Initializer + +``` text +new S3ToLambda(scope: Construct, id: string, props: S3ToLambdaProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`S3ToLambdaProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function. If set to false, you must provide an existing function for the `existingLambdaObj` property.| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|An optional, existing Lambda function. This property is required if `deployLambda` is set to false.| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user-provided props to override the default props for the Lambda function. This property is only required if `deployLambda` is set to true.| +|deployBucket?|`boolean`|Whether to create a S3 Bucket or use an existing S3 Bucket| +|existingBucketObj?|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Existing instance of S3 Bucket object| +|bucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|Optional user provided props to override the default props for S3 Bucket| +|s3EventSourceProps?|[`S3EventSourceProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda-event-sources.S3EventSourceProps.html)|Optional user provided props to override the default props for S3EventSourceProps| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Returns an instance of the lambda.Function created by the construct| +|s3Bucket()|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Returns an instance of the s3.Bucket created by the construct| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/architecture.png new file mode 100644 index 000000000..5f46107d5 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/lib/index.ts new file mode 100644 index 000000000..7847c2feb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/lib/index.ts @@ -0,0 +1,172 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as lambda from '@aws-cdk/aws-lambda'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as iam from '@aws-cdk/aws-iam'; +import { Construct, Stack } from '@aws-cdk/core'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { S3EventSourceProps, S3EventSource } from '@aws-cdk/aws-lambda-event-sources'; + +/** + * @summary The properties for the S3ToLambda class. + */ +export interface S3ToLambdaProps { + /** + * Whether to create a new lambda function or use an existing lambda function. + * If set to false, you must provide a lambda function object as `existingObj` + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly lambdaFunctionProps?: lambda.FunctionProps, + /** + * Whether to create a S3 Bucket or use an existing S3 Bucket. + * If set to false, you must provide S3 Bucket as `existingBucketObj` + * + * @default - true + */ + readonly deployBucket?: boolean, + /** + * Existing instance of S3 Bucket object. + * If `deployBucket` is set to false only then this property is required + * + * @default - None + */ + readonly existingBucketObj?: s3.Bucket, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly bucketProps?: s3.BucketProps, + /** + * Optional user provided props to override the default props + * + * @default - Default props are used + */ + readonly s3EventSourceProps?: S3EventSourceProps +} + +export class S3ToLambda extends Construct { + // Private variables + private fn: lambda.Function; + private bucket: s3.Bucket; + /** + * @summary Constructs a new instance of the IotToLambda class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {S3ToLambdaProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: S3ToLambdaProps) { + super(scope, id); + + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + + this.bucket = defaults.buildS3Bucket(scope, { + deployBucket: props.deployBucket, + existingBucketObj: props.existingBucketObj, + bucketProps: props.bucketProps + }); + + // Create S3 trigger to invoke lambda function + this.fn.addEventSource(new S3EventSource(this.bucket, + defaults.S3EventSourceProps(props.s3EventSourceProps))); + + this.addCfnNagSuppress(scope); + } + + private addCfnNagSuppress(scope: Construct) { + // Extract the CfnBucket from the s3Bucket + const s3BucketResource = this.bucket.node.findChild('Resource') as s3.CfnBucket; + + s3BucketResource.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W51', + reason: `This S3 bucket Bucket does not need a bucket policy` + }] + } + }; + + const root = Stack.of(scope); + const logicalId = 'BucketNotificationsHandler050a0587b7544547bf325f094a3db834'; + const notificationsResourceHandler = root.node.tryFindChild(logicalId) as lambda.Function; + const notificationsResourceHandlerRoleRole = notificationsResourceHandler.node.findChild('Role') as iam.Role; + const notificationsResourceHandlerRolePolicy = notificationsResourceHandlerRoleRole.node.findChild('DefaultPolicy') as iam.Policy; + + // Extract the CfnFunction from the Function + const fnResource = notificationsResourceHandler.node.findChild('Resource') as lambda.CfnFunction; + + fnResource.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W58', + reason: `Lambda function has permission to write CloudWatch Logs via AWSLambdaBasicExecutionRole policy attached to the lambda role` + }] + } + }; + + // Extract the CfnPolicy from the iam.Policy + const policyResource = notificationsResourceHandlerRolePolicy.node.findChild('Resource') as iam.CfnPolicy; + + policyResource.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W12', + reason: `Bucket resource is '*' due to circular dependency with bucket and role creation at the same time` + }] + } + }; + } + + /** + * @summary Returns an instance of the lambda.Function created by the construct. + * @returns {lambda.Function} Instance of the Function created by the construct. + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } + + /** + * @summary Returns an instance of the s3.Bucket created by the construct. + * @returns {s3.Bucket} Instance of the Bucket created by the construct. + * @since 0.8.0 + * @access public + */ + public s3Bucket(): s3.Bucket { + return this.bucket; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/package.json b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/package.json new file mode 100644 index 000000000..b02028928 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/package.json @@ -0,0 +1,83 @@ +{ + "name": "@aws-solutions-konstruk/aws-s3-lambda", + "version": "0.8.0", + "description": "CDK Constructs for AWS S3 to AWS Lambda integration", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-s3-lambda" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.s3lambda", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "s3lambda" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.S3Lambda", + "packageId": "Amazon.Konstruk.AWS.S3Lambda", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-s3-lambda", + "module": "aws_solutions_konstruk.aws_s3_lambda" + } + } + }, + "dependencies": { + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-cdk/aws-s3-notifications": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-lambda-event-sources": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-cdk/aws-lambda-event-sources": "~1.25.0", + "@aws-cdk/aws-s3-notifications": "~1.25.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/__snapshots__/s3-lambda.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/__snapshots__/s3-lambda.test.js.snap new file mode 100644 index 000000000..5e7815b1c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/__snapshots__/s3-lambda.test.js.snap @@ -0,0 +1,434 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test S3ToLambda default params 1`] = ` +Object { + "Parameters": Object { + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8ArtifactHash8D9AD644": Object { + "Description": "Artifact hash for asset \\"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\\"", + "Type": "String", + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB": Object { + "Description": "S3 bucket for asset \\"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\\"", + "Type": "String", + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7": Object { + "Description": "S3 key for asset version \\"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\\"", + "Type": "String", + }, + }, + "Resources": Object { + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691": Object { + "DependsOn": Array [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda function has permission to write CloudWatch Logs via AWSLambdaBasicExecutionRole policy attached to the lambda role", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "ZipFile": "exports.handler = (event, context) => { + // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies + const s3 = new (require('aws-sdk').S3)(); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const https = require(\\"https\\"); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const url = require(\\"url\\"); + log(JSON.stringify(event, undefined, 2)); + const props = event.ResourceProperties; + if (event.RequestType === 'Delete') { + props.NotificationConfiguration = {}; // this is how you clean out notifications + } + const req = { + Bucket: props.BucketName, + NotificationConfiguration: props.NotificationConfiguration + }; + return s3.putBucketNotificationConfiguration(req, (err, data) => { + log({ err, data }); + if (err) { + return submitResponse(\\"FAILED\\", err.message + \`\\\\nMore information in CloudWatch Log Stream: \${context.logStreamName}\`); + } + else { + return submitResponse(\\"SUCCESS\\"); + } + }); + function log(obj) { + console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj); + } + // tslint:disable-next-line:max-line-length + // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule + // to allow sending an error messge as a reason. + function submitResponse(responseStatus, reason) { + const responseBody = JSON.stringify({ + Status: responseStatus, + Reason: reason || \\"See the details in CloudWatch Log Stream: \\" + context.logStreamName, + PhysicalResourceId: event.PhysicalResourceId || event.LogicalResourceId, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + NoEcho: false, + }); + log({ responseBody }); + const parsedUrl = url.parse(event.ResponseURL); + const options = { + hostname: parsedUrl.hostname, + port: 443, + path: parsedUrl.path, + method: \\"PUT\\", + headers: { + \\"content-type\\": \\"\\", + \\"content-length\\": responseBody.length + } + }; + const request = https.request(options, (r) => { + log({ statusCode: r.statusCode, statusMessage: r.statusMessage }); + context.done(); + }); + request.on(\\"error\\", (error) => { + log({ sendError: error }); + context.done(); + }); + request.write(responseBody); + request.end(); + } +};", + }, + "Description": "AWS CloudFormation handler for \\"Custom::S3BucketNotifications\\" resources (@aws-cdk/aws-s3)", + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + "Timeout": 300, + }, + "Type": "AWS::Lambda::Function", + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Bucket resource is '*' due to circular dependency with bucket and role creation at the same time", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:PutBucketNotification", + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "Roles": Array [ + Object { + "Ref": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "LambdaFunctionAllowBucketNotificationsFromS3Bucket25B4F189": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "s3.amazonaws.com", + "SourceAccount": Object { + "Ref": "AWS::AccountId", + }, + "SourceArn": Object { + "Fn::GetAtt": Array [ + "S3Bucket07682993", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "S3Bucket07682993": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "S3LoggingBucket800A2B27", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "S3BucketNotifications58B5AD06": Object { + "DependsOn": Array [ + "LambdaFunctionAllowBucketNotificationsFromS3Bucket25B4F189", + ], + "Properties": Object { + "BucketName": Object { + "Ref": "S3Bucket07682993", + }, + "NotificationConfiguration": Object { + "LambdaFunctionConfigurations": Array [ + Object { + "Events": Array [ + "s3:ObjectCreated:*", + ], + "LambdaFunctionArn": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + }, + ], + }, + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn", + ], + }, + }, + "Type": "Custom::S3BucketNotifications", + }, + "S3LoggingBucket800A2B27": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/integ.existing-s3-bucket.expected.json b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/integ.existing-s3-bucket.expected.json new file mode 100644 index 000000000..f5a11688e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/integ.existing-s3-bucket.expected.json @@ -0,0 +1,366 @@ +{ + "Resources": { + "S3LoggingBucket800A2B27": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + }, + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "S3Bucket07682993": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "S3LoggingBucket800A2B27" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "S3BucketNotifications58B5AD06": { + "Type": "Custom::S3BucketNotifications", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn" + ] + }, + "BucketName": { + "Ref": "S3Bucket07682993" + }, + "NotificationConfiguration": { + "LambdaFunctionConfigurations": [ + { + "Events": [ + "s3:ObjectCreated:*" + ], + "LambdaFunctionArn": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + } + } + ] + } + }, + "DependsOn": [ + "LambdaFunctionAllowBucketNotificationsFromtests3lambdastackS3BucketAA5BB5A99E8B1157" + ] + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "LambdaFunctionAllowBucketNotificationsFromtests3lambdastackS3BucketAA5BB5A99E8B1157": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "s3.amazonaws.com", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "SourceArn": { + "Fn::GetAtt": [ + "S3Bucket07682993", + "Arn" + ] + } + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": { + "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" + ] + ] + } + ] + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutBucketNotification", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "Roles": [ + { + "Ref": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Bucket resource is '*' due to circular dependency with bucket and role creation at the same time" + } + ] + } + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", + "Code": { + "ZipFile": "exports.handler = (event, context) => {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies\n const s3 = new (require('aws-sdk').S3)();\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const https = require(\"https\");\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const url = require(\"url\");\n log(JSON.stringify(event, undefined, 2));\n const props = event.ResourceProperties;\n if (event.RequestType === 'Delete') {\n props.NotificationConfiguration = {}; // this is how you clean out notifications\n }\n const req = {\n Bucket: props.BucketName,\n NotificationConfiguration: props.NotificationConfiguration\n };\n return s3.putBucketNotificationConfiguration(req, (err, data) => {\n log({ err, data });\n if (err) {\n return submitResponse(\"FAILED\", err.message + `\\nMore information in CloudWatch Log Stream: ${context.logStreamName}`);\n }\n else {\n return submitResponse(\"SUCCESS\");\n }\n });\n function log(obj) {\n console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj);\n }\n // tslint:disable-next-line:max-line-length\n // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule\n // to allow sending an error messge as a reason.\n function submitResponse(responseStatus, reason) {\n const responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: reason || \"See the details in CloudWatch Log Stream: \" + context.logStreamName,\n PhysicalResourceId: event.PhysicalResourceId || event.LogicalResourceId,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: false,\n });\n log({ responseBody });\n const parsedUrl = url.parse(event.ResponseURL);\n const options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: \"PUT\",\n headers: {\n \"content-type\": \"\",\n \"content-length\": responseBody.length\n }\n };\n const request = https.request(options, (r) => {\n log({ statusCode: r.statusCode, statusMessage: r.statusMessage });\n context.done();\n });\n request.on(\"error\", (error) => {\n log({ sendError: error });\n context.done();\n });\n request.write(responseBody);\n request.end();\n }\n};" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Timeout": 300 + }, + "DependsOn": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda function has permission to write CloudWatch Logs via AWSLambdaBasicExecutionRole policy attached to the lambda role" + } + ] + } + } + } + }, + "Parameters": { + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB": { + "Type": "String", + "Description": "S3 bucket for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7": { + "Type": "String", + "Description": "S3 key for asset version \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8ArtifactHash8D9AD644": { + "Type": "String", + "Description": "Artifact hash for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/integ.existing-s3-bucket.ts b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/integ.existing-s3-bucket.ts new file mode 100644 index 000000000..fb126fa64 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/integ.existing-s3-bucket.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { S3ToLambda, S3ToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as defaults from '@aws-solutions-konstruk/core'; +const app = new App(); + +// Empty arguments +const stack = new Stack(app, 'test-s3-lambda-stack'); + +const myBucket: s3.Bucket = defaults.buildS3Bucket(stack, {}); + +// Extract the CfnBucket from the s3Bucket +const s3BucketResource = myBucket.node.findChild('Resource') as s3.CfnBucket; + +s3BucketResource.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W51', + reason: `This S3 bucket Bucket does not need a bucket policy` + }] + } +}; + +const props: S3ToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, + deployBucket: false, + existingBucketObj: myBucket +}; + +new S3ToLambda(stack, 'test-s3-lambda', props); +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..4d1829de0 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/integ.no-arguments.expected.json @@ -0,0 +1,366 @@ +{ + "Resources": { + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "LambdaFunctionAllowBucketNotificationsFromtests3lambdastackS3BucketAA5BB5A99E8B1157": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "s3.amazonaws.com", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "SourceArn": { + "Fn::GetAtt": [ + "S3Bucket07682993", + "Arn" + ] + } + } + }, + "S3LoggingBucket800A2B27": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + }, + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "S3Bucket07682993": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "S3LoggingBucket800A2B27" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "S3BucketNotifications58B5AD06": { + "Type": "Custom::S3BucketNotifications", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn" + ] + }, + "BucketName": { + "Ref": "S3Bucket07682993" + }, + "NotificationConfiguration": { + "LambdaFunctionConfigurations": [ + { + "Events": [ + "s3:ObjectCreated:*" + ], + "LambdaFunctionArn": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + } + } + ] + } + }, + "DependsOn": [ + "LambdaFunctionAllowBucketNotificationsFromtests3lambdastackS3BucketAA5BB5A99E8B1157" + ] + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": { + "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" + ] + ] + } + ] + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutBucketNotification", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "Roles": [ + { + "Ref": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Bucket resource is '*' due to circular dependency with bucket and role creation at the same time" + } + ] + } + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", + "Code": { + "ZipFile": "exports.handler = (event, context) => {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies\n const s3 = new (require('aws-sdk').S3)();\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const https = require(\"https\");\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const url = require(\"url\");\n log(JSON.stringify(event, undefined, 2));\n const props = event.ResourceProperties;\n if (event.RequestType === 'Delete') {\n props.NotificationConfiguration = {}; // this is how you clean out notifications\n }\n const req = {\n Bucket: props.BucketName,\n NotificationConfiguration: props.NotificationConfiguration\n };\n return s3.putBucketNotificationConfiguration(req, (err, data) => {\n log({ err, data });\n if (err) {\n return submitResponse(\"FAILED\", err.message + `\\nMore information in CloudWatch Log Stream: ${context.logStreamName}`);\n }\n else {\n return submitResponse(\"SUCCESS\");\n }\n });\n function log(obj) {\n console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj);\n }\n // tslint:disable-next-line:max-line-length\n // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule\n // to allow sending an error messge as a reason.\n function submitResponse(responseStatus, reason) {\n const responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: reason || \"See the details in CloudWatch Log Stream: \" + context.logStreamName,\n PhysicalResourceId: event.PhysicalResourceId || event.LogicalResourceId,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: false,\n });\n log({ responseBody });\n const parsedUrl = url.parse(event.ResponseURL);\n const options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: \"PUT\",\n headers: {\n \"content-type\": \"\",\n \"content-length\": responseBody.length\n }\n };\n const request = https.request(options, (r) => {\n log({ statusCode: r.statusCode, statusMessage: r.statusMessage });\n context.done();\n });\n request.on(\"error\", (error) => {\n log({ sendError: error });\n context.done();\n });\n request.write(responseBody);\n request.end();\n }\n};" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Timeout": 300 + }, + "DependsOn": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda function has permission to write CloudWatch Logs via AWSLambdaBasicExecutionRole policy attached to the lambda role" + } + ] + } + } + } + }, + "Parameters": { + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB": { + "Type": "String", + "Description": "S3 bucket for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7": { + "Type": "String", + "Description": "S3 key for asset version \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8ArtifactHash8D9AD644": { + "Type": "String", + "Description": "Artifact hash for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/integ.no-arguments.ts new file mode 100644 index 000000000..b32792b34 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/integ.no-arguments.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "@aws-cdk/core"; +import { S3ToLambda, S3ToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +const app = new App(); + +// Empty arguments +const stack = new Stack(app, 'test-s3-lambda-stack'); + +const props: S3ToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, +}; + +new S3ToLambda(stack, 'test-s3-lambda', props); +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/lambda/index.js new file mode 100644 index 000000000..743e4fdbb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/lambda/index.js @@ -0,0 +1,8 @@ +exports.handler = async function(event) { + console.log('request:', JSON.stringify(event, undefined, 2)); + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: `Hello, CDK! You've hit ${event.path}\n` + }; + }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/s3-lambda.test.ts b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/s3-lambda.test.ts new file mode 100644 index 000000000..3533808a8 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-s3-lambda/test/s3-lambda.test.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { S3ToLambda, S3ToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from "@aws-cdk/core"; +import '@aws-cdk/assert/jest'; + +function deployNewFunc(stack: cdk.Stack) { + const props: S3ToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, + }; + + return new S3ToLambda(stack, 'test-s3-lambda', props); +} + +test('snapshot test S3ToLambda default params', () => { + const stack = new cdk.Stack(); + deployNewFunc(stack); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: S3ToLambda = deployNewFunc(stack); + + expect(construct.lambdaFunction()).toBeInstanceOf(lambda.Function); + expect(construct.s3Bucket()).toBeInstanceOf(s3.Bucket); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/README.md b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/README.md new file mode 100644 index 000000000..5fc948a44 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/README.md @@ -0,0 +1,83 @@ +# aws-sns-lambda module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-sns-lambda/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_sns_lambda`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-sns-lambda`| + +This AWS Solutions Konstruk implements an Amazon SNS connected to an AWS Lambda function. + +Here is a minimal deployable pattern definition: + +``` javascript +const { SnsToLambdaProps, SnsToLambda } = require('@aws-solutions-konstruk/aws-sns-lambda'); + +const stack = new Stack(app, 'test-sns-lambda'); + +// Definitions +const props: SnsToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } +}; + +new SnsToLambda(stack, 'test-sns-lambda', props); + +``` + +## Initializer + +``` text +new SnsToLambda(scope: Construct, id: string, props: SnsToLambdaProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`S3ToLambdaProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function. If set to false, you must provide an existing function for the `existingLambdaObj` property.| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|An optional, existing Lambda function. This property is required if `deployLambda` is set to false.| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user-provided props to override the default props for the lambda function| +|topicProps?|[`sns.TopicProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sns.TopicProps.html)|Optional user provided properties to override the default properties for the SNS topic.| +|enableEncryption?|`boolean`|Use a KMS Key, either managed by this CDK app, or imported. If importing an encryption key, it must be specified in the encryptionKey property for this construct.| +|encryptionKey?|[`kms.Key`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kms.Key.html)|An optional, imported encryption key to encrypt the SNS topic with.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Returns an instance of the Lambda function created by the pattern.| +|snsTopic()|[`sns.Topic`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sns.Topic.html)|Returns an instance of the SNS topic created by the pattern.| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/architecture.png new file mode 100644 index 000000000..8a207f511 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/lib/index.ts new file mode 100644 index 000000000..970706eec --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/lib/index.ts @@ -0,0 +1,122 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sns from '@aws-cdk/aws-sns'; +import * as kms from '@aws-cdk/aws-kms'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; +import { SnsEventSource } from '@aws-cdk/aws-lambda-event-sources'; + +/** + * @summary The properties for the SnsToLambda class. + */ +export interface SnsToLambdaProps { + /** + * Whether to create a new Lambda function or use an existing Lambda function. + * If set to false, you must provide an existing function for the `existingLambdaObj` property. + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided properties to override the default properties for the Lambda function. + * If `deploy` is set to true only then this property is required. + * + * @default - Default properties are used. + */ + readonly lambdaFunctionProps?: lambda.FunctionProps | any + /** + * Optional user provided properties to override the default properties for the SNS topic. + * + * @default - Default properties are used. + */ + readonly topicProps?: sns.TopicProps | any + /** + * Use a KMS Key, either managed by this CDK app, or imported. If importing an encryption key, it must be specified in + * the encryptionKey property for this construct. + * + * @default - true (encryption enabled, managed by this CDK app). + */ + readonly enableEncryption?: boolean + /** + * An optional, imported encryption key to encrypt the SNS topic with. + * + * @default - not specified. + */ + readonly encryptionKey?: kms.Key +} + +/** + * @summary The SnsToLambda class. + */ +export class SnsToLambda extends Construct { + // Private variables + private fn: lambda.Function; + private topic: sns.Topic; + + /** + * @summary Constructs a new instance of the LambdaToSns class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {LambdaToSnsProps} props - user provided props for the construct. + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: SnsToLambdaProps) { + super(scope, id); + + // Setup the Lambda function + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + + // Setup the SNS topic + this.topic = defaults.buildTopic(this, { + enableEncryption: props.enableEncryption, + encryptionKey: props.encryptionKey + }); + + this.fn.addEventSource(new SnsEventSource(this.topic)); + + } + + /** + * @summary Returns an instance of the lambda.Function created by the construct. + * @returns {lambda.Function} Instance of the Function created by the construct. + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } + + /** + * @summary Returns an instance of the sns.Topic created by the construct. + * @returns {sns.Topic} Instance of the Topic created by the construct. + * @since 0.8.0 + * @access public + */ + public snsTopic(): sns.Topic { + return this.topic; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/package.json b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/package.json new file mode 100644 index 000000000..15812c402 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/package.json @@ -0,0 +1,83 @@ +{ + "name": "@aws-solutions-konstruk/aws-sns-lambda", + "version": "0.8.0", + "description": "CDK Constructs for AWS SNS to AWS Lambda integration", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-sns-lambda" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.snslambda", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "snslambda" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.SnsLambda", + "packageId": "Amazon.Konstruk.AWS.SnsLambda", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-sns-lambda", + "module": "aws_solutions_konstruk.aws_sns_lambda" + } + } + }, + "dependencies": { + "@aws-cdk/aws-sns": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-lambda-event-sources": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-kms": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-sns": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-lambda-event-sources": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-cdk/aws-kms": "~1.25.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/__snapshots__/sns-lambda.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/__snapshots__/sns-lambda.test.js.snap new file mode 100644 index 000000000..bb96c684a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/__snapshots__/sns-lambda.test.js.snap @@ -0,0 +1,233 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test SnsToLambda default params 1`] = ` +Object { + "Parameters": Object { + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8ArtifactHash8D9AD644": Object { + "Description": "Artifact hash for asset \\"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\\"", + "Type": "String", + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB": Object { + "Description": "S3 bucket for asset \\"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\\"", + "Type": "String", + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7": Object { + "Description": "S3 key for asset version \\"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionAllowInvoketestsnslambdaSnsTopicEB0543A084BF6FA6": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "sns.amazonaws.com", + "SourceArn": Object { + "Ref": "testsnslambdaSnsTopic52CA159E", + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionSnsTopic70134CE0": Object { + "Properties": Object { + "Endpoint": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Protocol": "lambda", + "TopicArn": Object { + "Ref": "testsnslambdaSnsTopic52CA159E", + }, + }, + "Type": "AWS::SNS::Subscription", + }, + "testsnslambdaEncryptionKeyDDDF040B": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "testsnslambdaSnsTopic52CA159E": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Ref": "testsnslambdaEncryptionKeyDDDF040B", + }, + }, + "Type": "AWS::SNS::Topic", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/integ.no-arguments.expected.json new file mode 100644 index 000000000..0dd935987 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/integ.no-arguments.expected.json @@ -0,0 +1,230 @@ +{ + "Description": "Integration Test for aws-sns-lambda", + "Resources": { + "testsnslambdaEncryptionKeyDDDF040B": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "EnableKeyRotation": true + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testsnslambdaSnsTopic52CA159E": { + "Type": "AWS::SNS::Topic", + "Properties": { + "KmsMasterKeyId": { + "Ref": "testsnslambdaEncryptionKeyDDDF040B" + } + } + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "LambdaFunctionAllowInvoketestsnslambdaSnsTopic02988CE1827B8D82": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "testsnslambdaSnsTopic52CA159E" + } + } + }, + "LambdaFunctionSnsTopic70134CE0": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "testsnslambdaSnsTopic52CA159E" + }, + "Endpoint": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + } + } + } + }, + "Parameters": { + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3Bucket9E1964CB": { + "Type": "String", + "Description": "S3 bucket for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8S3VersionKey7153CEE7": { + "Type": "String", + "Description": "S3 key for asset version \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + }, + "AssetParameters0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8ArtifactHash8D9AD644": { + "Type": "String", + "Description": "Artifact hash for asset \"0c3255e93ffe7a906c7422e9f0e9cc4c7fd86ee996ee3bb302e2f134b38463c8\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/integ.no-arguments.ts b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/integ.no-arguments.ts new file mode 100644 index 000000000..4b446cbd0 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/integ.no-arguments.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { SnsToLambda, SnsToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-sns-lambda'); +stack.templateOptions.description = 'Integration Test for aws-sns-lambda'; + +// Definitions +const props: SnsToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } +}; + +new SnsToLambda(stack, 'test-sns-lambda', props); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/lambda/index.js new file mode 100644 index 000000000..743e4fdbb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/lambda/index.js @@ -0,0 +1,8 @@ +exports.handler = async function(event) { + console.log('request:', JSON.stringify(event, undefined, 2)); + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: `Hello, CDK! You've hit ${event.path}\n` + }; + }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/sns-lambda.test.ts b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/sns-lambda.test.ts new file mode 100644 index 000000000..3db6f010e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sns-lambda/test/sns-lambda.test.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { SnsToLambda, SnsToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from "@aws-cdk/core"; +import '@aws-cdk/assert/jest'; +import { Topic } from '@aws-cdk/aws-sns'; + +function deployNewFunc(stack: cdk.Stack) { + const props: SnsToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, + }; + + return new SnsToLambda(stack, 'test-sns-lambda', props); +} + +test('snapshot test SnsToLambda default params', () => { + const stack = new cdk.Stack(); + deployNewFunc(stack); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check getter methods', () => { + const stack = new cdk.Stack(); + + const construct: SnsToLambda = deployNewFunc(stack); + + expect(construct.lambdaFunction()).toBeInstanceOf(lambda.Function); + expect(construct.snsTopic()).toBeInstanceOf(Topic); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/.eslintignore b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/.gitignore b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/.npmignore b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/README.md b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/README.md new file mode 100644 index 000000000..250527ad9 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/README.md @@ -0,0 +1,81 @@ +# aws-sqs-lambda module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/aws-sqs-lambda/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png){: style="height:16px;width:16px"} Python|`aws_solutions_konstruk.aws_sns_lambda`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png){: style="height:16px;width:16px"} Typescript|`@aws-solutions-konstruk/aws-sns-lambda`| + +This AWS Solutions Konstruk implements an Amazon SQS queue connected to an AWS Lambda function. + +Here is a minimal deployable pattern definition: + +``` javascript +const { SqsToLambda } = require('@aws-solutions-konstruk/aws-sqs-lambda'); + +new SqsToLambda(stack, 'SqsToLambdaPattern', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + deployDeadLetterQueue: true, + maxReceiveCount: 15 +}); + +``` + +## Initializer + +``` text +new SqsToLambda(scope: Construct, id: string, props: SqsToLambdaProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`SqsToLambdaProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function.| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|An optional, existing Lambda function.| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user-provided props to override the default props for the Lambda function.| +|queueProps?|[`sqs.QueueProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sqs.QueueProps.html)|Optional user-provided props to override the default props for the SQS queue.| +|encryptionKeyProps?|[`kms.KeyProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-kms.KeyProps.html)|Optional user-provided props to override the default props for the KMS encryption key.| +|deployDeadLetterQueue|`boolean`|Whether to create a secondary queue to be used as a dead letter queue.| +|maxReceiveCount|`number`|The number of times a message can be unsuccesfully dequeued before being moved to the dead letter queue.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|lambdaFunction()|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Returns an instance of the Lambda function created by the pattern.| +|sqsQueue()|[`sqs.Queue`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sqs.Queue.html)|Returns an instance of the SQS queue created by the pattern.| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/architecture.png b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/architecture.png new file mode 100644 index 000000000..917d5e875 Binary files /dev/null and b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/architecture.png differ diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/lib/index.ts b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/lib/index.ts new file mode 100644 index 000000000..5dbac146a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/lib/index.ts @@ -0,0 +1,146 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import * as sqs from '@aws-cdk/aws-sqs'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as kms from '@aws-cdk/aws-kms'; +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; +import { SqsEventSource } from '@aws-cdk/aws-lambda-event-sources'; + +/** + * @summary The properties for the SqsToLambda class. + */ +export interface SqsToLambdaProps { + /** + * Whether to create a new Lambda function or use an existing Lambda function. + * If set to false, you must provide an existing function for the `existingLambdaObj` property. + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided properties to override the default properties for the Lambda function. + * If `deploy` is set to true only then this property is required. + * + * @default - Default properties are used. + */ + readonly lambdaFunctionProps?: lambda.FunctionProps | any + /** + * Optional user provided properties + * + * @default - Default props are used + */ + readonly queueProps?: sqs.QueueProps | any + /** + * Optional user provided props to override the default props for the KMS. + * + * @default - Default props are used + */ + readonly encryptionKeyProps?: kms.KeyProps | any + /** + * Whether to deploy a secondary queue to be used as a dead letter queue. + * + * @default - required field. + */ + readonly deployDeadLetterQueue: boolean, + /** + * The number of times a message can be unsuccesfully dequeued before being moved to the dead-letter queue. + * + * @default - required field. + */ + readonly maxReceiveCount: number +} + +/** + * @summary The SqsToLambda class. + */ +export class SqsToLambda extends Construct { + // Private variables + private queue: sqs.Queue; + private fn: lambda.Function; + private encryptionKey: kms.Key; + + /** + * @summary Constructs a new instance of the SqsToLambda class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {CloudFrontToApiGatewayToLambdaProps} props - user provided props for the construct. + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: SqsToLambdaProps) { + super(scope, id); + + // Setup the encryption key + this.encryptionKey = defaults.buildEncryptionKey(scope, props.encryptionKeyProps); + + // Setup the Lambda function + this.fn = defaults.buildLambdaFunction(scope, { + deployLambda: props.deployLambda, + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps + }); + + // Setup the dead letter queue, if applicable + let dlqi: sqs.DeadLetterQueue | undefined; + if (props.deployDeadLetterQueue) { + const dlq: sqs.Queue = defaults.buildQueue(scope, 'deadLetterQueue', { + encryptionKey: this.encryptionKey, + queueProps: props.queueProps + }); + dlqi = defaults.buildDeadLetterQueue({ + deadLetterQueue: dlq, + maxReceiveCount: props.maxReceiveCount + }); + } + + // Setup the queue + this.queue = defaults.buildQueue(scope, 'queue', { + encryptionKey: this.encryptionKey, + queueProps: props.queueProps, + deadLetterQueue: dlqi + }); + + // Setup the event source mapping + this.fn.addEventSource(new SqsEventSource(this.queue)); + } + + /** + * @summary Returns an instance of the lambda.Function created by the construct. + * @returns {lambda.Function} Instance of the Function created by the construct. + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.fn; + } + + /** + * @summary Returns an instance of the sqs.Queue created by the construct. + * @returns {sqs.Queue} Instance of the Queue created by the construct. + * @since 0.8.0 + * @access public + */ + public sqsQueue(): sqs.Queue { + return this.queue; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/package.json b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/package.json new file mode 100644 index 000000000..78cffbe9b --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/package.json @@ -0,0 +1,80 @@ +{ + "name": "@aws-solutions-konstruk/aws-sqs-lambda", + "version": "0.8.0", + "description": "CDK constructs for defining an interaction between an Amazon SQS queue and an AWS Lambda function.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/aws-sqs-lambda" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.sqslambda", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "sqslambda" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.SqsLambda", + "packageId": "Amazon.Konstruk.AWS.SqsLambda", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.aws-sqs-lambda", + "module": "aws_solutions_konstruk.aws_sqs_lambda" + } + } + }, + "dependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-lambda-event-sources": "~1.25.0", + "@aws-cdk/aws-sqs": "~1.25.0", + "@aws-cdk/aws-kms": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-lambda-event-sources": "~1.25.0", + "@aws-cdk/aws-sqs": "~1.25.0", + "@aws-cdk/aws-kms": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/__snapshots__/test.sqs-lambda.test.js.snap b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/__snapshots__/test.sqs-lambda.test.js.snap new file mode 100644 index 000000000..02aae6d9a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/__snapshots__/test.sqs-lambda.test.js.snap @@ -0,0 +1,534 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pattern deployment w/ Existing Lambda Function 1`] = ` +Object { + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + Object { + "Action": "kms:Decrypt", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::GetAtt": Array [ + "ExistingLambdaFunctionServiceRole7CC6DE65", + "Arn", + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "ExistingLambdaFunctionF606C520": Object { + "DependsOn": Array [ + "ExistingLambdaFunctionServiceRoleDefaultPolicy2431D213", + "ExistingLambdaFunctionServiceRole7CC6DE65", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "ExistingLambdaFunctionServiceRole7CC6DE65", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "ExistingLambdaFunctionServiceRole7CC6DE65": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "ExistingLambdaFunctionServiceRoleDefaultPolicy2431D213": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "sqs:ReceiveMessage", + "sqs:ChangeMessageVisibility", + "sqs:GetQueueUrl", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "Arn", + ], + }, + }, + Object { + "Action": "kms:Decrypt", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "ExistingLambdaFunctionServiceRoleDefaultPolicy2431D213", + "Roles": Array [ + Object { + "Ref": "ExistingLambdaFunctionServiceRole7CC6DE65", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "ExistingLambdaFunctionSqsEventSourcequeue85681C04": Object { + "Properties": Object { + "EventSourceArn": Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "Arn", + ], + }, + "FunctionName": Object { + "Ref": "ExistingLambdaFunctionF606C520", + }, + }, + "Type": "AWS::Lambda::EventSourceMapping", + }, + "queue276F7297": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + "Type": "AWS::SQS::Queue", + }, + }, +} +`; + +exports[`Pattern deployment w/ new Lambda function and default props 1`] = ` +Object { + "Parameters": Object { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": Object { + "Description": "Artifact hash for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": Object { + "Description": "S3 bucket for asset \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": Object { + "Description": "S3 key for asset version \\"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\\"", + "Type": "String", + }, + }, + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + Object { + "Action": "kms:Decrypt", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "sqs:ReceiveMessage", + "sqs:ChangeMessageVisibility", + "sqs:GetQueueUrl", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "Arn", + ], + }, + }, + Object { + "Action": "kms:Decrypt", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "LambdaFunctionSqsEventSourcequeue7A15E0AF": Object { + "Properties": Object { + "EventSourceArn": Object { + "Fn::GetAtt": Array [ + "queue276F7297", + "Arn", + ], + }, + "FunctionName": Object { + "Ref": "LambdaFunctionBF21E41F", + }, + }, + "Type": "AWS::Lambda::EventSourceMapping", + }, + "deadLetterQueue3F848E28": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + "Type": "AWS::SQS::Queue", + }, + "queue276F7297": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + "RedrivePolicy": Object { + "deadLetterTargetArn": Object { + "Fn::GetAtt": Array [ + "deadLetterQueue3F848E28", + "Arn", + ], + }, + "maxReceiveCount": 15, + }, + }, + "Type": "AWS::SQS::Queue", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/integ.deployFunction.expected.json b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/integ.deployFunction.expected.json new file mode 100644 index 000000000..de6b099dc --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/integ.deployFunction.expected.json @@ -0,0 +1,292 @@ +{ + "Description": "Integration Test for aws-sqs-lambda", + "Resources": { + "EncryptionKey1B843E66": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": "kms:Decrypt", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "EnableKeyRotation": true + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "sqs:ReceiveMessage", + "sqs:ChangeMessageVisibility", + "sqs:GetQueueUrl", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "queue276F7297", + "Arn" + ] + } + }, + { + "Action": "kms:Decrypt", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "LambdaFunctionSqsEventSourcetestsqslambdaqueue583E2E6C926C265C": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "EventSourceArn": { + "Fn::GetAtt": [ + "queue276F7297", + "Arn" + ] + }, + "FunctionName": { + "Ref": "LambdaFunctionBF21E41F" + } + } + }, + "deadLetterQueue3F848E28": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + } + } + }, + "queue276F7297": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + }, + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "deadLetterQueue3F848E28", + "Arn" + ] + }, + "maxReceiveCount": 3 + } + } + } + }, + "Parameters": { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": { + "Type": "String", + "Description": "S3 bucket for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": { + "Type": "String", + "Description": "S3 key for asset version \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": { + "Type": "String", + "Description": "Artifact hash for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/integ.deployFunction.ts b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/integ.deployFunction.ts new file mode 100644 index 000000000..6858f97c5 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/integ.deployFunction.ts @@ -0,0 +1,41 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { SqsToLambda, SqsToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-sqs-lambda'); +stack.templateOptions.description = 'Integration Test for aws-sqs-lambda'; + +// Definitions +const props: SqsToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + queueProps: {}, + deployDeadLetterQueue: true, + maxReceiveCount: 3, + encryptionKeyProps: {} +}; + +new SqsToLambda(stack, 'test-sqs-lambda', props); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/integ.existingFunction.expected.json b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/integ.existingFunction.expected.json new file mode 100644 index 000000000..2531f650d --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/integ.existingFunction.expected.json @@ -0,0 +1,292 @@ +{ + "Description": "Integration Test for aws-sqs-lambda", + "Resources": { + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "sqs:ReceiveMessage", + "sqs:ChangeMessageVisibility", + "sqs:GetQueueUrl", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "queue276F7297", + "Arn" + ] + } + }, + { + "Action": "kms:Decrypt", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1" + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "LambdaFunctionSqsEventSourcetestsqslambdaqueue583E2E6C926C265C": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "EventSourceArn": { + "Fn::GetAtt": [ + "queue276F7297", + "Arn" + ] + }, + "FunctionName": { + "Ref": "LambdaFunctionBF21E41F" + } + } + }, + "EncryptionKey1B843E66": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": "kms:Decrypt", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "EnableKeyRotation": true + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "deadLetterQueue3F848E28": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + } + } + }, + "queue276F7297": { + "Type": "AWS::SQS::Queue", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "EncryptionKey1B843E66", + "Arn" + ] + }, + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "deadLetterQueue3F848E28", + "Arn" + ] + }, + "maxReceiveCount": 3 + } + } + } + }, + "Parameters": { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": { + "Type": "String", + "Description": "S3 bucket for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": { + "Type": "String", + "Description": "S3 key for asset version \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": { + "Type": "String", + "Description": "Artifact hash for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/integ.existingFunction.ts b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/integ.existingFunction.ts new file mode 100644 index 000000000..63337bc51 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/integ.existingFunction.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { SqsToLambda, SqsToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as defaults from '@aws-solutions-konstruk/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-sqs-lambda'); +stack.templateOptions.description = 'Integration Test for aws-sqs-lambda'; + +// Definitions +const lambdaFunctionProps = { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) +}; + +const func = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + +const props: SqsToLambdaProps = { + deployLambda: false, + existingLambdaObj: func, + queueProps: {}, + deployDeadLetterQueue: true, + maxReceiveCount: 3, + encryptionKeyProps: {} +}; + +new SqsToLambda(stack, 'test-sqs-lambda', props); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/lambda/index.js new file mode 100644 index 000000000..51fdc6953 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/lambda/index.js @@ -0,0 +1,21 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +exports.handler = async (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); +    return { +      statusCode: 200, +      headers: { 'Content-Type': 'text/plain' }, +      body: `Hello from Project Vesper! You've hit ${event.path}\n` +    }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/test.sqs-lambda.test.ts b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/test.sqs-lambda.test.ts new file mode 100644 index 000000000..a83f2945e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/aws-sqs-lambda/test/test.sqs-lambda.test.ts @@ -0,0 +1,163 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { Stack } from "@aws-cdk/core"; +import { SqsToLambda, SqsToLambdaProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +// -------------------------------------------------------------- +// Pattern deployment w/ new Lambda function and +// default properties +// -------------------------------------------------------------- +test('Pattern deployment w/ new Lambda function and default props', () => { + // Initial Setup + const stack = new Stack(); + const props: SqsToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + deployDeadLetterQueue: true, + maxReceiveCount: 15, + queueProps: {}, + encryptionKeyProps: {}, + }; + new SqsToLambda(stack, 'test-sqs-lambda', props); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Pattern deployment w/ new Lambda function and +// overridden properties +// -------------------------------------------------------------- +test('Pattern deployment w/ new Lambda function and overridden props', () => { + // Initial Setup + const stack = new Stack(); + const props: SqsToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`), + environment: { + OVERRIDE: "TRUE" + } + }, + queueProps: { + fifo: true + }, + encryptionKeyProps: {}, + deployDeadLetterQueue: false, + maxReceiveCount: 0 + }; + const app = new SqsToLambda(stack, 'test-sqs-lambda', props); + // Assertion 1 + expect(app.lambdaFunction()).toHaveProperty('environment.OVERRIDE', 'TRUE'); + // Assertion 2 + expect(app.sqsQueue()).toHaveProperty('fifo', true); +}); + +// -------------------------------------------------------------- +// Pattern Deployment w/ Existing Lambda function +// -------------------------------------------------------------- +test('Pattern deployment w/ Existing Lambda Function', () => { + // Initial Setup + const stack = new Stack(); + const fn = new lambda.Function(stack, 'ExistingLambdaFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }); + const props: SqsToLambdaProps = { + deployLambda: false, + existingLambdaObj: fn, + deployDeadLetterQueue: false, + encryptionKeyProps: {}, + maxReceiveCount: 0, + queueProps: {} + }; + new SqsToLambda(stack, 'test-apigateway-lambda', props); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test the getter methods +// -------------------------------------------------------------- +test('Test getter methods', () => { + // Initial Setup + const stack = new Stack(); + const props: SqsToLambdaProps = { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }, + deployDeadLetterQueue: false, + encryptionKeyProps: {}, + maxReceiveCount: 0, + queueProps: {} + }; + const app = new SqsToLambda(stack, 'test-apigateway-lambda', props); + // Assertion 1 + expect(app.lambdaFunction()).toBeDefined(); + // Assertion 2 + expect(app.sqsQueue()).toBeDefined(); +}); + +// -------------------------------------------------------------- +// Test error handling for existing Lambda function +// -------------------------------------------------------------- +test('Test error handling for existing Lambda function', () => { + // Initial Setup + const stack = new Stack(); + const props: SqsToLambdaProps = { + deployLambda: false, + existingLambdaObj: undefined, + deployDeadLetterQueue: false, + encryptionKeyProps: {}, + maxReceiveCount: 0, + queueProps: {} + }; + // Assertion 1 + expect(() => { + new SqsToLambda(stack, 'test-sqs-lambda', props); + }).toThrowError(); +}); + +// -------------------------------------------------------------- +// Test error handling for new Lambda function +// w/o required properties +// -------------------------------------------------------------- +test('Test error handling for new Lambda function w/o required properties', () => { + // Initial Setup + const stack = new Stack(); + const props: SqsToLambdaProps = { + deployLambda: true, + deployDeadLetterQueue: false, + encryptionKeyProps: {}, + maxReceiveCount: 0, + queueProps: {} + }; + // Assertion 1 + expect(() => { + new SqsToLambda(stack, 'test-sqs-lambda', props); + }).toThrowError(); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/.eslintignore b/source/patterns/@aws-solutions-konstruk/core/.eslintignore new file mode 100644 index 000000000..23c247780 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/.eslintignore @@ -0,0 +1,7 @@ +index.js +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js +test/lambda-test/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/.gitignore b/source/patterns/@aws-solutions-konstruk/core/.gitignore new file mode 100644 index 000000000..f4b17fa0b --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/.gitignore @@ -0,0 +1,17 @@ +index.js +lib/*.js +test/*.js +!test/lambda*/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/.npmignore b/source/patterns/@aws-solutions-konstruk/core/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/README.md b/source/patterns/@aws-solutions-konstruk/core/README.md new file mode 100644 index 000000000..9ecafd285 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/README.md @@ -0,0 +1,69 @@ +# core module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> **This is a _developer preview_ (public beta) module.** +> +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **API Reference**:| http://docs.awssolutionsbuilder.com/aws-solutions-konstruk/latest/api/core/| +|:-------------|:-------------| +
+ +The core library includes the basic building blocks of the AWS Solutions Konstruk Library. It defines the core classes that are used in the rest of the AWS Solutions Konstruk Library. + +## Default Properties for AWS CDK Constructs + +Core library sets the default properties for the AWS CDK Constructs used by the AWS Solutions Konstruk Library constructs. + +For example, the following is the snippet of default properties for S3 Bucket construct created by AWS Solutions Konstruk construct. By default, it will turn on the server-side encryption, bucket versioning, block all public access and setup the S3 access logging. + +``` +{ + encryption: s3.BucketEncryption.S3_MANAGED, + versioned: true, + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + removalPolicy: RemovalPolicy.RETAIN, + serverAccessLogsBucket: loggingBucket +} +``` + +## Override the default properties + +The default properties set by the Core library can be overriden by user provided properties. For example, the user can override the Amazon S3 Block Public Access property to meet specific requirements. + +``` + const stack = new cdk.Stack(); + + const props: CloudFrontToS3Props = { + deployBucket: true, + bucketProps: { + blockPublicAccess: { + blockPublicAcls: false, + blockPublicPolicy: true, + ignorePublicAcls: false, + restrictPublicBuckets: true + } + } + }; + + new CloudFrontToS3(stack, 'test-cloudfront-s3', props); + + expect(stack).toHaveResource("AWS::S3::Bucket", { + PublicAccessBlockConfiguration: { + BlockPublicAcls: false, + BlockPublicPolicy: true, + IgnorePublicAcls: false, + RestrictPublicBuckets: true + }, + }); +``` + diff --git a/source/patterns/@aws-solutions-konstruk/core/index.ts b/source/patterns/@aws-solutions-konstruk/core/index.ts new file mode 100644 index 000000000..a8de6950e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/index.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +export * from './lib/apigateway-defaults'; +export * from './lib/apigateway-helper'; +export * from './lib/dynamodb-table-defaults'; +export * from './lib/iot-topic-rule-defaults'; +export * from './lib/kinesis-analytics-defaults'; +export * from './lib/kinesis-analytics-helper'; +export * from './lib/kinesis-streams-defaults'; +export * from './lib/kinesis-streams-helper'; +export * from './lib/kinesis-firehose-s3-defaults'; +export * from './lib/lambda-event-source-mapping-defaults'; +export * from './lib/kms-defaults'; +export * from './lib/kms-helper'; +export * from './lib/lambda-defaults'; +export * from './lib/lambda-helper'; +export * from './lib/s3-bucket-defaults'; +export * from './lib/s3-bucket-helper'; +export * from './lib/sns-defaults'; +export * from './lib/sns-helper'; +export * from './lib/sqs-defaults'; +export * from './lib/sqs-helper'; +export * from './lib/cloudwatch-log-group-defaults'; +export * from './lib/cloudfront-distribution-helper'; +export * from './lib/cloudfront-distribution-defaults'; +export * from './lib/utils'; +export * from './lib/events-rule-defaults'; +export * from './lib/cognito-defaults'; +export * from './lib/cognito-helper'; +export * from './lib/elasticsearch-defaults'; +export * from './lib/elasticsearch-helper'; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/apigateway-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/apigateway-defaults.ts new file mode 100644 index 000000000..03340c9eb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/apigateway-defaults.ts @@ -0,0 +1,69 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as api from '@aws-cdk/aws-apigateway'; +import * as lambda from '@aws-cdk/aws-lambda'; + +export function DefaultGlobalLambdaRestApiProps(_existingLambdaObj: lambda.Function) { + const defaultGatewayProps: api.LambdaRestApiProps = { + handler: _existingLambdaObj, + options: { + endpointTypes: [api.EndpointType.EDGE], + cloudWatchRole: false, + // Configure API Gateway Execution logging + deployOptions: { + loggingLevel: api.MethodLoggingLevel.INFO, + dataTraceEnabled: true + }, + defaultMethodOptions: { + authorizationType: api.AuthorizationType.IAM + } + } + }; + return defaultGatewayProps; +} + +export function DefaultRegionalLambdaRestApiProps(_existingLambdaObj: lambda.Function) { + const defaultGatewayProps: api.LambdaRestApiProps = { + handler: _existingLambdaObj, + options: { + endpointTypes: [api.EndpointType.REGIONAL], + cloudWatchRole: false, + // Configure API Gateway Execution logging + deployOptions: { + loggingLevel: api.MethodLoggingLevel.INFO, + dataTraceEnabled: true + }, + defaultMethodOptions: { + authorizationType: api.AuthorizationType.IAM + } + } + }; + return defaultGatewayProps; +} + +export function DefaultGlobalApiProps() { + const defaultGatewayProps: api.RestApiProps = { + endpointTypes: [api.EndpointType.EDGE], + cloudWatchRole: false, + // Configure API Gateway Execution logging + deployOptions: { + loggingLevel: api.MethodLoggingLevel.INFO, + dataTraceEnabled: true + }, + defaultMethodOptions: { + authorizationType: api.AuthorizationType.IAM + } + }; + return defaultGatewayProps; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/apigateway-helper.ts b/source/patterns/@aws-solutions-konstruk/core/lib/apigateway-helper.ts new file mode 100644 index 000000000..bb5fc0c1c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/apigateway-helper.ts @@ -0,0 +1,175 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + + // Imports +import * as logs from '@aws-cdk/aws-logs'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import * as api from '@aws-cdk/aws-apigateway'; +import * as iam from '@aws-cdk/aws-iam'; +import * as apiDefaults from './apigateway-defaults'; +import { DefaultLogGroupProps } from './cloudwatch-log-group-defaults'; +import { overrideProps } from './utils'; + +/** + * Creates and configures an api.LambdaRestApi. + * @param scope - the construct to which the LambdaRestApi should be attached to. + * @param defaultApiGatewayProps - the default properties for the LambdaRestApi. + * @param apiGatewayProps - (optional) user-specified properties to override the default properties. + */ +function configureLambdaRestApi(scope: cdk.Construct, defaultApiGatewayProps: api.LambdaRestApiProps, + apiGatewayProps?: api.LambdaRestApiProps): api.RestApi { + // Define the API object + let _api: api.RestApi; + if (apiGatewayProps) { + // If property overrides have been provided, incorporate them and deploy + const _apiGatewayProps = overrideProps(defaultApiGatewayProps, apiGatewayProps); + _api = new api.LambdaRestApi(scope, 'RestApi', _apiGatewayProps); + } else { + // If no property overrides, deploy using the default configuration + _api = new api.LambdaRestApi(scope, 'RestApi', defaultApiGatewayProps); + } + // Configure API access logging + configureApiAccessLogging(scope, _api); + + // Configure Usage Plan + _api.addUsagePlan('UsagePlan', { + apiStages: [{ + api: _api, + stage: _api.deploymentStage + }] + }); + + // Return the API object + return _api; +} + +/** + * Create and configures access logging for API Gateway resources. + * @param scope - the construct to which the access logging capabilities should be attached to. + * @param _api - an existing api.RestApi or api.LambdaRestApi. + */ +function configureApiAccessLogging(scope: cdk.Construct, _api: api.RestApi): void { + // Configure log group for API Gateway AccessLogging + const logGroup = new logs.LogGroup(scope, 'ApiAccessLogGroup', DefaultLogGroupProps()); + // Configure the API stage + const stage: api.CfnStage = _api.deploymentStage.node.findChild('Resource') as api.CfnStage; + stage.accessLogSetting = { + destinationArn: logGroup.logGroupArn, + format: "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + }; + // Configure the API deployment + const deployment: api.CfnDeployment = _api.latestDeployment?.node.findChild('Resource') as api.CfnDeployment; + deployment.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W45', + reason: `ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource` + }] + } + }; + // Setup the IAM Role for API Gateway CloudWatch access + const restApiCloudwatchRole = new iam.Role(scope, 'LambdaRestApiCloudWatchRole', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), + inlinePolicies: { + LambdaRestApiCloudWatchRolePolicy: new iam.PolicyDocument({ + statements: [new iam.PolicyStatement({ + actions: [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:DescribeLogGroups', + 'logs:DescribeLogStreams', + 'logs:PutLogEvents', + 'logs:GetLogEvents', + 'logs:FilterLogEvents' + ], + resources: [`arn:aws:logs:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:*`] + }) + ] + }) + } + }); + // Create and configure AWS::ApiGateway::Account with CloudWatch Role for ApiGateway + const CfnApi = _api.node.findChild('Resource') as api.CfnRestApi; + const cfnAccount: api.CfnAccount = new api.CfnAccount(scope, 'LambdaRestApiAccount', { + cloudWatchRoleArn: restApiCloudwatchRole.roleArn + }); + cfnAccount.addDependsOn(CfnApi); +} + +/** + * Creates and configures an api.RestApi. + * @param scope - the construct to which the RestApi should be attached to. + * @param defaultApiGatewayProps - the default properties for the RestApi. + * @param apiGatewayProps - (optional) user-specified properties to override the default properties. + */ +function configureRestApi(scope: cdk.Construct, defaultApiGatewayProps: api.RestApiProps, + apiGatewayProps?: api.RestApiProps): api.RestApi { + // Define the API + let _api: api.RestApi; + if (apiGatewayProps) { + // If property overrides have been provided, incorporate them and deploy + const _apiGatewayProps = overrideProps(defaultApiGatewayProps, apiGatewayProps); + _api = new api.RestApi(scope, 'RestApi', _apiGatewayProps); + } else { + // If no property overrides, deploy using the default configuration + _api = new api.RestApi(scope, 'RestApi', defaultApiGatewayProps); + } + // Configure API access logging + configureApiAccessLogging(scope, _api); + + // Configure Usage Plan + _api.addUsagePlan('UsagePlan', { + apiStages: [{ + api: _api, + stage: _api.deploymentStage + }] + }); + + // Return the API + return _api; +} + +/** + * Builds and returns a global api.RestApi designed to be used with an AWS Lambda function. + * @param scope - the construct to which the RestApi should be attached to. + * @param _existingLambdaObj - an existing AWS Lambda function. + * @param apiGatewayProps - (optional) user-specified properties to override the default properties. + */ +export function GlobalLambdaRestApi(scope: cdk.Construct, _existingLambdaObj: lambda.Function, + apiGatewayProps?: api.LambdaRestApiProps): api.RestApi { + const defaultProps = apiDefaults.DefaultGlobalLambdaRestApiProps(_existingLambdaObj); + return configureLambdaRestApi(scope, defaultProps, apiGatewayProps); +} + +/** + * Builds and returns a regional api.RestApi designed to be used with an AWS Lambda function. + * @param scope - the construct to which the RestApi should be attached to. + * @param _existingLambdaObj - an existing AWS Lambda function. + * @param apiGatewayProps - (optional) user-specified properties to override the default properties. + */ +export function RegionalLambdaRestApi(scope: cdk.Construct, _existingLambdaObj: lambda.Function, + apiGatewayProps?: api.LambdaRestApiProps): api.RestApi { + const defaultProps = apiDefaults.DefaultRegionalLambdaRestApiProps(_existingLambdaObj); + return configureLambdaRestApi(scope, defaultProps, apiGatewayProps); +} + +/** + * Builds and returns a standard api.RestApi. + * @param scope - the construct to which the RestApi should be attached to. + * @param apiGatewayProps - (optional) user-specified properties to override the default properties. + */ +export function GlobalRestApi(scope: cdk.Construct, apiGatewayProps?: api.RestApiProps): api.RestApi { + const defaultProps = apiDefaults.DefaultGlobalApiProps(); + return configureRestApi(scope, defaultProps, apiGatewayProps); +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/cloudfront-distribution-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/cloudfront-distribution-defaults.ts new file mode 100644 index 000000000..d23a0e990 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/cloudfront-distribution-defaults.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as api from '@aws-cdk/aws-apigateway'; +import * as cdk from '@aws-cdk/core'; + +export function DefaultCloudFrontWebDistributionForApiGatewayProps(apiEndPoint: api.RestApi, + loggingBucket?: s3.Bucket): cloudfront.CloudFrontWebDistributionProps { + const apiEndPointUrlWithoutProtocol = cdk.Fn.select(1, cdk.Fn.split("://", apiEndPoint.url)); + const apiEndPointDomainName = cdk.Fn.select(0, cdk.Fn.split("/", apiEndPointUrlWithoutProtocol)); + + if (loggingBucket) { + return { + originConfigs: [{ + customOriginSource: { + domainName: apiEndPointDomainName + }, + behaviors: [{ + isDefaultBehavior: true, + }] + }], + loggingConfig: { + bucket: loggingBucket + } + } as cloudfront.CloudFrontWebDistributionProps; + } else { + return { + originConfigs: [{ + customOriginSource: { + domainName: apiEndPointDomainName + }, + behaviors: [{ + isDefaultBehavior: true, + }] + }] + } as cloudfront.CloudFrontWebDistributionProps; + } +} + +export function DefaultCloudFrontWebDistributionForS3Props(sourceBucket: s3.Bucket, loggingBucket: s3.Bucket, + _originAccessIdentity: cloudfront.IOriginAccessIdentity): + cloudfront.CloudFrontWebDistributionProps { + const cfDistributionProps: cloudfront.CloudFrontWebDistributionProps = { + originConfigs: [ { + s3OriginSource: { + s3BucketSource: sourceBucket, + originAccessIdentity: _originAccessIdentity + }, + behaviors: [ { + isDefaultBehavior: true, + } ] + } ], + loggingConfig: { + bucket: loggingBucket + } + } as cloudfront.CloudFrontWebDistributionProps; + return cfDistributionProps; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/cloudfront-distribution-helper.ts b/source/patterns/@aws-solutions-konstruk/core/lib/cloudfront-distribution-helper.ts new file mode 100644 index 000000000..b4c277e9e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/cloudfront-distribution-helper.ts @@ -0,0 +1,126 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as iam from '@aws-cdk/aws-iam'; +import * as api from '@aws-cdk/aws-apigateway'; +import { DefaultS3Props } from './s3-bucket-defaults'; +import { DefaultCloudFrontWebDistributionForS3Props, DefaultCloudFrontWebDistributionForApiGatewayProps } from './cloudfront-distribution-defaults'; +import { overrideProps } from './utils'; + +export function CloudFrontDistributionForApiGateway(scope: cdk.Construct, apiEndPoint: api.RestApi, cloudFrontDistributionProps?: + cloudfront.CloudFrontWebDistributionProps | any): cloudfront.CloudFrontWebDistribution { + + let defaultprops; + + if (cloudFrontDistributionProps && cloudFrontDistributionProps.loggingBucket) { + defaultprops = DefaultCloudFrontWebDistributionForApiGatewayProps(apiEndPoint); + } else { + // Create the Logging Bucket + const loggingBucket: s3.Bucket = new s3.Bucket(scope, 'CloudfrontLoggingBucket', DefaultS3Props()); + + // Extract the CfnBucket from the loggingBucket + const loggingBucketResource = loggingBucket.node.findChild('Resource') as s3.CfnBucket; + + // Override accessControl configuration and add metadata for the logging bucket + loggingBucketResource.addPropertyOverride('AccessControl', 'LogDeliveryWrite'); + loggingBucketResource.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W35', + reason: `This S3 bucket is used as the access logging bucket for CloudFront Distribution` + }, { + id: 'W51', + reason: `This S3 bucket is used as the access logging bucket for CloudFront Distribution` + }] + } + }; + + defaultprops = DefaultCloudFrontWebDistributionForApiGatewayProps(apiEndPoint, loggingBucket); + } + + // Create the Cloudfront Distribution + let cfprops = defaultprops; + if (cloudFrontDistributionProps) { + cfprops = overrideProps(defaultprops, cloudFrontDistributionProps); + } + const cfDistribution: cloudfront.CloudFrontWebDistribution = new cloudfront.CloudFrontWebDistribution(scope, 'CloudFrontDistribution', cfprops); + + return cfDistribution; +} + +export function CloudFrontDistributionForS3(scope: cdk.Construct, sourceBucket: s3.Bucket, cloudFrontDistributionProps?: + cloudfront.CloudFrontWebDistributionProps | any): cloudfront.CloudFrontWebDistribution { + // Create the Logging Bucket + const loggingBucket: s3.Bucket = new s3.Bucket(scope, 'CloudfrontLoggingBucket', DefaultS3Props()); + + // Extract the CfnBucket from the loggingBucket + const loggingBucketResource = loggingBucket.node.findChild('Resource') as s3.CfnBucket; + + // Override accessControl configuration and add metadata for the logging bucket + loggingBucketResource.addPropertyOverride('AccessControl', 'LogDeliveryWrite'); + loggingBucketResource.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W35', + reason: `This S3 bucket is used as the access logging bucket for CloudFront Distribution` + }, { + id: 'W51', + reason: `This S3 bucket is used as the access logging bucket for CloudFront Distribution` + }] + } + }; + + // Create CloudFront Origin Access Identity User + const cfnOrigAccessId = new cloudfront.CfnCloudFrontOriginAccessIdentity(scope, 'CloudFrontOriginAccessIdentity', { + cloudFrontOriginAccessIdentityConfig: { + comment: 'Access S3 bucket content only through CloudFront' + } + }); + + const oaiImported = cloudfront.OriginAccessIdentity.fromOriginAccessIdentityName( + scope, + 'OAIImported', + cfnOrigAccessId.ref + ); + + // Create the Cloudfront Distribution + const defaultprops = DefaultCloudFrontWebDistributionForS3Props(sourceBucket, loggingBucket, oaiImported); + let cfprops = defaultprops; + if (cloudFrontDistributionProps) { + cfprops = overrideProps(defaultprops, cloudFrontDistributionProps); + } + const cfDistribution: cloudfront.CloudFrontWebDistribution = new cloudfront.CloudFrontWebDistribution(scope, 'CloudFrontDistribution', cfprops); + + // Add S3 Bucket Policy to allow s3:GetObject for CloudFront Origin Access Identity User + sourceBucket.addToResourcePolicy(new iam.PolicyStatement({ + actions: ['s3:GetObject'], + resources: [sourceBucket.arnForObjects('*')], + principals: [new iam.CanonicalUserPrincipal(cfnOrigAccessId.attrS3CanonicalUserId)] + })); + + // Extract the CfnBucketPolicy from the sourceBucket + const bucketPolicy = sourceBucket.policy as s3.BucketPolicy; + const sourceBucketPolicy = bucketPolicy.node.findChild('Resource') as s3.CfnBucketPolicy; + sourceBucketPolicy.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'F16', + reason: `Public website bucket policy requires a wildcard principal` + }] + } + }; + return cfDistribution; +} diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/cloudwatch-log-group-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/cloudwatch-log-group-defaults.ts new file mode 100644 index 000000000..141fe174a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/cloudwatch-log-group-defaults.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as logs from '@aws-cdk/aws-logs'; + +export function DefaultLogGroupProps(_logGroupName: string = ''): logs.LogGroupProps { + if (_logGroupName !== '') { + return { + logGroupName: _logGroupName, + retention: logs.RetentionDays.INFINITE + } as logs.LogGroupProps; + } else { + return { + retention: logs.RetentionDays.INFINITE + } as logs.LogGroupProps; + } +} diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/cognito-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/cognito-defaults.ts new file mode 100644 index 000000000..b761570b1 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/cognito-defaults.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cognito from '@aws-cdk/aws-cognito'; + +const DefaultUserPoolProps: cognito.UserPoolProps = { +}; + +export function DefaultIdentityPoolProps(userPoolClientId: string, userPoolProviderName: string): cognito.CfnIdentityPoolProps { + return { + allowUnauthenticatedIdentities: false, + cognitoIdentityProviders: [{ + clientId: userPoolClientId, + providerName: userPoolProviderName, + serverSideTokenCheck: true + }] + } as cognito.CfnIdentityPoolProps; +} + +export function DefaultUserPoolClientProps(userpool: cognito.UserPool): cognito.UserPoolClientProps { + return { + userPool: userpool + } as cognito.UserPoolClientProps; +} + +export { DefaultUserPoolProps }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/cognito-helper.ts b/source/patterns/@aws-solutions-konstruk/core/lib/cognito-helper.ts new file mode 100644 index 000000000..37d0651f1 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/cognito-helper.ts @@ -0,0 +1,118 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cognito from '@aws-cdk/aws-cognito'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { overrideProps } from './utils'; +import { DefaultUserPoolProps, DefaultUserPoolClientProps, DefaultIdentityPoolProps } from './cognito-defaults'; + +export interface CognitoOptions { + readonly identitypool: cognito.CfnIdentityPool, + readonly userpool: cognito.UserPool, + readonly userpoolclient: cognito.UserPoolClient +} + +export function buildUserPool(scope: cdk.Construct, userPoolProps?: cognito.UserPoolProps): cognito.UserPool { + let cognitoUserPoolProps: cognito.UserPoolProps; + + if (userPoolProps) { + cognitoUserPoolProps = overrideProps(DefaultUserPoolProps, userPoolProps); + } else { + cognitoUserPoolProps = DefaultUserPoolProps; + } + + const userPool = new cognito.UserPool(scope, 'CognitoUserPool', cognitoUserPoolProps); + + // Set the advancedSecurityMode to ENFORCED + const cfnUserPool = userPool.node.findChild('Resource') as cognito.CfnUserPool; + + cfnUserPool.userPoolAddOns = { + advancedSecurityMode: 'ENFORCED' + }; + + return userPool; +} + +export function buildUserPoolClient(scope: cdk.Construct, userPool: cognito.UserPool, + cognitoUserPoolClientProps?: cognito.UserPoolClientProps): cognito.UserPoolClient { + + let userPoolClientProps: cognito.UserPoolClientProps; + + if (cognitoUserPoolClientProps) { + userPoolClientProps = overrideProps(DefaultUserPoolClientProps(userPool), cognitoUserPoolClientProps); + } else { + userPoolClientProps = DefaultUserPoolClientProps(userPool); + } + + return new cognito.UserPoolClient(scope, 'CognitoUserPoolClient', userPoolClientProps); +} + +export function buildIdentityPool(scope: cdk.Construct, userpool: cognito.UserPool, userpoolclient: cognito.UserPoolClient, + identityPoolProps?: cognito.CfnIdentityPoolProps): cognito.CfnIdentityPool { + + let cognitoIdentityPoolProps: cognito.CfnIdentityPoolProps = DefaultIdentityPoolProps(userpoolclient.userPoolClientId, + userpool.userPoolProviderName); + + if (identityPoolProps) { + cognitoIdentityPoolProps = overrideProps(cognitoIdentityPoolProps, identityPoolProps); + } + + const idPool = new cognito.CfnIdentityPool(scope, 'CognitoIdentityPool', cognitoIdentityPoolProps); + + return idPool; +} + +export function setupCognitoForElasticSearch(scope: cdk.Construct, domainName: string, options: CognitoOptions): iam.Role { + + // Create the domain for Cognito UserPool + const userpooldomain = new cognito.CfnUserPoolDomain(scope, 'UserPoolDomain', { + domain: domainName, + userPoolId: options.userpool.userPoolId + }); + userpooldomain.addDependsOn(options.userpool.node.findChild('Resource') as cognito.CfnUserPool); + + // Setup the IAM Role for Cognito Authorized Users + const cognitoPrincipal = new iam.FederatedPrincipal( + 'cognito-identity.amazonaws.com', + { + 'StringEquals': { 'cognito-identity.amazonaws.com:aud': options.identitypool.ref }, + 'ForAnyValue:StringLike': { 'cognito-identity.amazonaws.com:amr': 'authenticated' } + }, + 'sts:AssumeRoleWithWebIdentity'); + + const cognitoAuthorizedRole = new iam.Role(scope, 'CognitoAuthorizedRole', { + assumedBy: cognitoPrincipal, + inlinePolicies: { + CognitoAccessPolicy: new iam.PolicyDocument({ + statements: [new iam.PolicyStatement({ + actions: [ + 'es:ESHttp*' + ], + resources: [`arn:aws:es:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:domain/${domainName}/*`] + }) + ] + }) + } + }); + + // Attach the IAM Role for Cognito Authorized Users + new cognito.CfnIdentityPoolRoleAttachment(scope, 'IdentityPoolRoleMapping', { + identityPoolId: options.identitypool.ref, + roles: { + authenticated: cognitoAuthorizedRole.roleArn + } + }); + + return cognitoAuthorizedRole; +} diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/dynamodb-table-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/dynamodb-table-defaults.ts new file mode 100644 index 000000000..a3921a915 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/dynamodb-table-defaults.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as dynamodb from '@aws-cdk/aws-dynamodb'; + +const DefaultTableProps: dynamodb.TableProps = { + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + serverSideEncryption: true, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + } +}; + +const DefaultTableWithStreamProps: dynamodb.TableProps = { + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + serverSideEncryption: true, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + }, + stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES +}; + +export { DefaultTableProps, DefaultTableWithStreamProps }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/elasticsearch-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/elasticsearch-defaults.ts new file mode 100644 index 000000000..e9f5b0190 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/elasticsearch-defaults.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as elasticsearch from '@aws-cdk/aws-elasticsearch'; +import * as cognito from '@aws-cdk/aws-cognito'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; + +export interface CfnDomainOptions { + readonly identitypool: cognito.CfnIdentityPool, + readonly userpool: cognito.UserPool, + readonly cognitoAuthorizedRoleARN: string, + readonly serviceRoleARN?: string +} + +export function DefaultCfnDomainProps(domainName: string, cognitoKibanaConfigureRole: iam.Role, options: CfnDomainOptions) { + const roleARNs: iam.IPrincipal[] = []; + + roleARNs.push(new iam.ArnPrincipal(options.cognitoAuthorizedRoleARN)); + + if (options.serviceRoleARN) { + roleARNs.push(new iam.ArnPrincipal(options.serviceRoleARN)); + } + + return { + domainName, + elasticsearchVersion: '6.3', + encryptionAtRestOptions: { + enabled: true + }, + nodeToNodeEncryptionOptions: { + enabled: true + }, + elasticsearchClusterConfig: { + dedicatedMasterEnabled: true, + dedicatedMasterCount: 3, + instanceCount: 3, + zoneAwarenessEnabled: true, + zoneAwarenessConfig: { + availabilityZoneCount: 3 + } + }, + snapshotOptions: { + automatedSnapshotStartHour: 1 + }, + ebsOptions: { + ebsEnabled: true, + volumeSize: 10 + }, + cognitoOptions: { + enabled: true, + identityPoolId: options.identitypool.ref, + userPoolId: options.userpool.userPoolId, + roleArn: cognitoKibanaConfigureRole.roleArn + }, + accessPolicies: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + principals: roleARNs, + actions: [ + 'es:ESHttp*' + ], + resources: [ + `arn:aws:es:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:domain/${domainName}/*` + ] + }) + ] + }) + } as elasticsearch.CfnDomainProps; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/elasticsearch-helper.ts b/source/patterns/@aws-solutions-konstruk/core/lib/elasticsearch-helper.ts new file mode 100644 index 000000000..2b5c8aab1 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/elasticsearch-helper.ts @@ -0,0 +1,218 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as elasticsearch from '@aws-cdk/aws-elasticsearch'; +import { CfnDomainOptions, DefaultCfnDomainProps } from './elasticsearch-defaults'; +import { overrideProps } from './utils'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; + +export function buildElasticSearch(scope: cdk.Construct, domainName: string, + options: CfnDomainOptions, cfnDomainProps?: elasticsearch.CfnDomainProps): elasticsearch.CfnDomain { + + // Setup the IAM Role & policy for ES to configure Cognito User pool and Identity pool + const cognitoKibanaConfigureRole = new iam.Role(scope, 'CognitoKibanaConfigureRole', { + assumedBy: new iam.ServicePrincipal('es.amazonaws.com') + }); + + const cognitoKibanaConfigureRolePolicy = new iam.Policy(scope, 'CognitoKibanaConfigureRolePolicy', { + statements: [ + new iam.PolicyStatement({ + actions: [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateElasticsearchDomainConfig" + ], + resources: [ + options.userpool.userPoolArn, + `arn:aws:cognito-identity:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:identitypool/${options.identitypool.ref}`, + `arn:aws:es:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:domain/${domainName}` + ] + }), + new iam.PolicyStatement({ + actions: [ + "iam:PassRole" + ], + conditions: { + StringLike: {'iam:PassedToService': 'cognito-identity.amazonaws.com'} + }, + resources: [ + cognitoKibanaConfigureRole.roleArn + ] + }) + ] + }); + cognitoKibanaConfigureRolePolicy.attachToRole(cognitoKibanaConfigureRole); + + let _cfnDomainProps = DefaultCfnDomainProps(domainName, cognitoKibanaConfigureRole, options); + + if (cfnDomainProps) { + _cfnDomainProps = overrideProps(_cfnDomainProps, cfnDomainProps); + } + + const esDomain = new elasticsearch.CfnDomain(scope, 'ElasticsearchDomain', _cfnDomainProps); + + esDomain.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W28', + reason: `The ES Domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific ES instance only` + }] + } + }; + + return esDomain; +} + +export function buildElasticSearchCWAlarms(scope: cdk.Construct): cloudwatch.Alarm[] { + // Setup CW Alarms for ES + const alarms: cloudwatch.Alarm[] = new Array(); + + // ClusterStatus.red maximum is >= 1 for 1 minute, 1 consecutive time + alarms.push(new cloudwatch.Alarm(scope, 'StatusRedAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'ClusterStatus.red' + }), + threshold: 1, + evaluationPeriods: 1, + statistic: 'Maximum', + period: cdk.Duration.seconds(60), + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'At least one primary shard and its replicas are not allocated to a node. ' + })); + + // ClusterStatus.yellow maximum is >= 1 for 1 minute, 1 consecutive time + alarms.push(new cloudwatch.Alarm(scope, 'StatusYellowAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'ClusterStatus.yellow' + }), + threshold: 1, + evaluationPeriods: 1, + statistic: 'Maximum', + period: cdk.Duration.seconds(60), + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'At least one replica shard is not allocated to a node.' + })); + + // FreeStorageSpace minimum is <= 20480 for 1 minute, 1 consecutive time + alarms.push(new cloudwatch.Alarm(scope, 'FreeStorageSpaceTooLowAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'FreeStorageSpace' + }), + threshold: 2000, + evaluationPeriods: 1, + statistic: 'Minimum', + period: cdk.Duration.seconds(60), + comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'A node in your cluster is down to 20 GiB of free storage space.' + })); + + // ClusterIndexWritesBlocked is >= 1 for 5 minutes, 1 consecutive time + alarms.push(new cloudwatch.Alarm(scope, 'IndexWritesBlockedTooHighAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'ClusterIndexWritesBlocked' + }), + threshold: 1, + evaluationPeriods: 1, + statistic: 'Maximum', + period: cdk.Duration.seconds(300), + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'Your cluster is blocking write requests.' + })); + + // AutomatedSnapshotFailure maximum is >= 1 for 1 minute, 1 consecutive time + alarms.push(new cloudwatch.Alarm(scope, 'AutomatedSnapshotFailureTooHighAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'AutomatedSnapshotFailure' + }), + threshold: 1, + evaluationPeriods: 1, + statistic: 'Maximum', + period: cdk.Duration.seconds(60), + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'An automated snapshot failed. This failure is often the result of a red cluster health status.' + })); + + // CPUUtilization maximum is >= 80% for 15 minutes, 3 consecutive times + alarms.push(new cloudwatch.Alarm(scope, 'CPUUtilizationTooHighAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'CPUUtilization' + }), + threshold: 80, + evaluationPeriods: 3, + statistic: 'Average', + period: cdk.Duration.seconds(900), + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: '100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.' + })); + + // JVMMemoryPressure maximum is >= 80% for 5 minutes, 3 consecutive times + alarms.push(new cloudwatch.Alarm(scope, 'JVMMemoryPressureTooHighAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'JVMMemoryPressure' + }), + threshold: 80, + evaluationPeriods: 1, + statistic: 'Average', + period: cdk.Duration.seconds(900), + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.' + })); + + // MasterCPUUtilization maximum is >= 50% for 15 minutes, 3 consecutive times + alarms.push(new cloudwatch.Alarm(scope, 'MasterCPUUtilizationTooHighAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'MasterCPUUtilization' + }), + threshold: 50, + evaluationPeriods: 3, + statistic: 'Average', + period: cdk.Duration.seconds(900), + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.' + })); + + // MasterJVMMemoryPressure maximum is >= 80% for 15 minutes, 1 consecutive time + alarms.push(new cloudwatch.Alarm(scope, 'MasterJVMMemoryPressureTooHighAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/ES', + metricName: 'MasterJVMMemoryPressure' + }), + threshold: 50, + evaluationPeriods: 1, + statistic: 'Average', + period: cdk.Duration.seconds(900), + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + alarmDescription: 'Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.' + })); + + return alarms; +} diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/events-rule-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/events-rule-defaults.ts new file mode 100644 index 000000000..2d34ca3b6 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/events-rule-defaults.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as events from '@aws-cdk/aws-events'; + +export function DefaultEventsRuleProps(_targets: events.IRuleTarget[]) { + const defaultEventsRuleProps: events.RuleProps = { + targets: _targets + }; + + return defaultEventsRuleProps; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/iot-topic-rule-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/iot-topic-rule-defaults.ts new file mode 100644 index 000000000..20c1173a2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/iot-topic-rule-defaults.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as iot from '@aws-cdk/aws-iot'; + +export function DefaultCfnTopicRuleProps(_actions: iot.CfnTopicRule.ActionProperty[], _sql: string = '') { + const _topicRulePayload: iot.CfnTopicRule.TopicRulePayloadProperty = { + ruleDisabled: false, + actions: _actions, + sql: _sql + }; + + const defaultCfnTopicRuleProps: iot.CfnTopicRuleProps = { + topicRulePayload: _topicRulePayload + }; + + return defaultCfnTopicRuleProps; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-analytics-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-analytics-defaults.ts new file mode 100644 index 000000000..a70ac5ada --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-analytics-defaults.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as kinesisanalytics from '@aws-cdk/aws-kinesisanalytics'; + +const DefaultCfnApplicationProps: kinesisanalytics.CfnApplicationProps = { + inputs: [] +}; + +export { DefaultCfnApplicationProps }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-analytics-helper.ts b/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-analytics-helper.ts new file mode 100644 index 000000000..aec263367 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-analytics-helper.ts @@ -0,0 +1,72 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + + // Imports + import * as kinesisAnalytics from '@aws-cdk/aws-kinesisanalytics'; + import * as kinesisFirehose from '@aws-cdk/aws-kinesisfirehose'; + import * as iam from '@aws-cdk/aws-iam'; + import * as defaults from './kinesis-analytics-defaults'; + import * as cdk from '@aws-cdk/core'; + import { overrideProps } from './utils'; + + export interface BuildKinesisAnalyticsAppProps { + /** + * A Kinesis Data Firehose for the Kinesis Streams application to connect to. + * + * @default - Default props are used + */ + readonly kinesisFirehose: kinesisFirehose.CfnDeliveryStream + /** + * Optional user provided props to override the default props for the Kinesis analytics app. + * + * @default - Default props are used + */ + readonly kinesisAnalyticsProps?: kinesisAnalytics.CfnApplicationProps | any + } + + export function buildKinesisAnalyticsApp(scope: cdk.Construct, props: BuildKinesisAnalyticsAppProps): kinesisAnalytics.CfnApplication { + + // Setup the IAM role for Kinesis Analytics + const analyticsRole = new iam.Role(scope, 'KinesisAnalyticsRole', { + assumedBy: new iam.ServicePrincipal('kinesisanalytics.amazonaws.com'), + }); + + // Setup the IAM policy for Kinesis Analytics + const analyticsPolicy = new iam.Policy(scope, 'KinesisAnalyticsPolicy', { + statements: [ + new iam.PolicyStatement({ + actions: [ + 'firehose:DescribeDeliveryStream', + 'firehose:Get*' + ], + resources: [props.kinesisFirehose.attrArn] + }) + ]}); + + // Attach policy to role + analyticsPolicy.attachToRole(analyticsRole); + + // Setup the Kinesis application properties + const kinesisAnalyticsProps = overrideProps(defaults.DefaultCfnApplicationProps, props.kinesisAnalyticsProps); + kinesisAnalyticsProps.inputs[0].kinesisFirehoseInput = { + resourceArn: props.kinesisFirehose.attrArn, + roleArn: analyticsRole.roleArn + }; + + // Setup the Kinesis application and add dependencies + const kinesisAnalyticsApp = new kinesisAnalytics.CfnApplication(scope, 'KinesisAnalytics', kinesisAnalyticsProps); + kinesisAnalyticsApp.addDependsOn(analyticsPolicy.node.findChild('Resource') as iam.CfnPolicy); + + // Create the application and return + return kinesisAnalyticsApp; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-firehose-s3-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-firehose-s3-defaults.ts new file mode 100644 index 000000000..3b478853e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-firehose-s3-defaults.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { CfnDeliveryStreamProps } from '@aws-cdk/aws-kinesisfirehose'; + +export function DefaultCfnDeliveryStreamProps(_bucketArn: string, _roleArn: string, + _logGroupName: string, _logStreamName: string): CfnDeliveryStreamProps { + return { + extendedS3DestinationConfiguration : { + bucketArn: _bucketArn, + bufferingHints: { + intervalInSeconds: 300, + sizeInMBs: 5 + }, + compressionFormat: 'GZIP', + roleArn: _roleArn, + cloudWatchLoggingOptions: { + enabled: true, + logGroupName: _logGroupName, + logStreamName: _logStreamName + } + } + } as CfnDeliveryStreamProps; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-streams-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-streams-defaults.ts new file mode 100644 index 000000000..ef20c82d8 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-streams-defaults.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as kinesis from '@aws-cdk/aws-kinesis'; + +const DefaultStreamProps: kinesis.StreamProps = { + encryption: kinesis.StreamEncryption.KMS +}; + +export { DefaultStreamProps }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-streams-helper.ts b/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-streams-helper.ts new file mode 100644 index 000000000..0f91ae987 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/kinesis-streams-helper.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + + // Imports +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as kms from '@aws-cdk/aws-kms'; +import { DefaultStreamProps } from './kinesis-streams-defaults'; +import * as cdk from '@aws-cdk/core'; +import { overrideProps } from './utils'; + +export interface BuildKinesisStreamProps { + /** + * Optional external encryption key to use for stream encryption. + * + * @default - Default props are used. + */ + readonly encryptionKey?: kms.Key + /** + * Optional user provided props to override the default props for the Kinesis stream. + * + * @default - Default props are used. + */ + readonly kinesisStreamProps?: kinesis.StreamProps | any +} + +export function buildKinesisStream(scope: cdk.Construct, props?: BuildKinesisStreamProps): kinesis.Stream { + // If props is undefined, define it + props = (props === undefined) ? {} : props; + // Setup the stream properties + let kinesisStreamProps; + if (props.hasOwnProperty('kinesisStreamProps')) { + // If property overrides have been provided, incorporate them and deploy + kinesisStreamProps = overrideProps(DefaultStreamProps, props.kinesisStreamProps); + } else { + // If no property overrides, deploy using the default configuration + kinesisStreamProps = DefaultStreamProps; + } + // Set conditional stream encryption properties + if (!kinesisStreamProps.hasOwnProperty('encryptionKey') && props.hasOwnProperty('kinesisStreamProps')) { + kinesisStreamProps.encryptionKey = props.encryptionKey; + } + // Create the stream and return + return new kinesis.Stream(scope, 'KinesisStream', kinesisStreamProps); +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/kms-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/kms-defaults.ts new file mode 100644 index 000000000..589eb8d22 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/kms-defaults.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { KeyProps } from '@aws-cdk/aws-kms'; + +const DefaultEncryptionProps: KeyProps = { + enableKeyRotation: true +}; + +export { DefaultEncryptionProps }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/kms-helper.ts b/source/patterns/@aws-solutions-konstruk/core/lib/kms-helper.ts new file mode 100644 index 000000000..40678d57c --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/kms-helper.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import * as kms from '@aws-cdk/aws-kms'; +import { DefaultEncryptionProps } from './kms-defaults'; +import * as cdk from '@aws-cdk/core'; +import { overrideProps } from './utils'; + +export interface BuildEncryptionKeyProps { + /** + * Optional user-provided props to override the default props for the encryption key. + * + * @default - Default props are used. + */ + readonly encryptionKeyProps?: kms.KeyProps | any +} + +export function buildEncryptionKey(scope: cdk.Construct, props?: BuildEncryptionKeyProps): kms.Key { + // If props is undefined, define it + props = (props === undefined) ? {} : props; + // Setup the key properties + let encryptionKeyProps; + if (props.hasOwnProperty('encryptionKeyProps')) { + // If property overrides have been provided, incorporate them and deploy + encryptionKeyProps = overrideProps(DefaultEncryptionProps, props.encryptionKeyProps); + } else { + // If no property overrides, deploy using the default configuration + encryptionKeyProps = DefaultEncryptionProps; + } + // Create the encryption key and return + return new kms.Key(scope, 'EncryptionKey', encryptionKeyProps); +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/lambda-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/lambda-defaults.ts new file mode 100644 index 000000000..8ecd5934f --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/lambda-defaults.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as lambda from '@aws-cdk/aws-lambda'; +import * as iam from '@aws-cdk/aws-iam'; + +export function DefaultLambdaFunctionProps(lambdaServiceRole: iam.Role): lambda.FunctionProps | any { + + const lambdaFunctionProps: lambda.FunctionProps | any = { + role: lambdaServiceRole + }; + + return lambdaFunctionProps; +} + +export function DefaultLambdaFunctionPropsForNodeJS(lambdaServiceRole: iam.Role): lambda.FunctionProps | any { + + const lambdaFunctionProps: lambda.FunctionProps | any = { + role: lambdaServiceRole, + environment: { + AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1' + } + }; + + return lambdaFunctionProps; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/lambda-event-source-mapping-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/lambda-event-source-mapping-defaults.ts new file mode 100644 index 000000000..1672b7c89 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/lambda-event-source-mapping-defaults.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as lambda from '@aws-cdk/aws-lambda'; +import { overrideProps } from './utils'; +import { DynamoEventSourceProps, S3EventSourceProps } from '@aws-cdk/aws-lambda-event-sources'; +import * as s3 from '@aws-cdk/aws-s3'; + +export function DefaultKinesisEventSourceProps(_eventSourceArn: string) { + const defaultEventSourceProps: lambda.EventSourceMappingOptions = { + eventSourceArn: _eventSourceArn + }; + return defaultEventSourceProps; +} + +export function DynamoEventSourceProps(_dynamoEventSourceProps?: DynamoEventSourceProps) { + + const defaultDynamoEventSourceProps = { + startingPosition: lambda.StartingPosition.TRIM_HORIZON + }; + + if (_dynamoEventSourceProps) { + return overrideProps(defaultDynamoEventSourceProps, _dynamoEventSourceProps); + } else { + return defaultDynamoEventSourceProps; + } +} + +export function S3EventSourceProps(_s3EventSourceProps?: S3EventSourceProps) { + + const defaultS3EventSourceProps: S3EventSourceProps = { + events: [s3.EventType.OBJECT_CREATED] + }; + + if (_s3EventSourceProps) { + return overrideProps(defaultS3EventSourceProps, _s3EventSourceProps, false); + } else { + return defaultS3EventSourceProps; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/lambda-helper.ts b/source/patterns/@aws-solutions-konstruk/core/lib/lambda-helper.ts new file mode 100644 index 000000000..c7e48e9ab --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/lambda-helper.ts @@ -0,0 +1,103 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as lambda from '@aws-cdk/aws-lambda'; +import * as iam from '@aws-cdk/aws-iam'; +import { DefaultLambdaFunctionProps, DefaultLambdaFunctionPropsForNodeJS } from './lambda-defaults'; +import * as cdk from '@aws-cdk/core'; +import { overrideProps } from './utils'; + +export interface BuildLambdaFunctionProps { + /** + * Whether to create a new Lambda function or use an existing Lambda function. + * If set to false, you must provide a lambda function object as `existingLambdaObj` + * + * @default - true + */ + readonly deployLambda: boolean, + /** + * Existing instance of Lambda Function object. + * If `deploy` is set to false only then this property is required + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * Optional user provided props to override the default props for the Lambda function. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly lambdaFunctionProps?: lambda.FunctionProps +} + +export function buildLambdaFunction(scope: cdk.Construct, props: BuildLambdaFunctionProps): lambda.Function { + // Conditional lambda function creation + // If deployLambda == false + if (props.hasOwnProperty('deployLambda') && props.deployLambda === false) { + if (props.existingLambdaObj) { + return props.existingLambdaObj; + } else { + throw Error('Missing existingObj from props for deployLambda = false'); + } + // If deployLambda == true + } else { + if (props.lambdaFunctionProps) { + return deployLambdaFunction(scope, props.lambdaFunctionProps); + } else { + throw Error('Missing lambdaFunctionProps from props for deployLambda = true'); + } + } +} + +export function deployLambdaFunction(scope: cdk.Construct, lambdaFunctionProps: lambda.FunctionProps): lambda.Function { + // Setup the IAM Role for Lambda Service + const lambdaServiceRole = new iam.Role(scope, 'LambdaFunctionServiceRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + inlinePolicies: { + LambdaFunctionServiceRolePolicy: new iam.PolicyDocument({ + statements: [new iam.PolicyStatement({ + actions: [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents' + ], + resources: [`arn:aws:logs:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:log-group:/aws/lambda/*`] + })] + }) + } + }); + + // Override the DefaultFunctionProps with user provided lambdaFunctionProps + let _lambdaFunctionProps = overrideProps(DefaultLambdaFunctionProps(lambdaServiceRole), lambdaFunctionProps); + + if (lambdaFunctionProps.runtime === lambda.Runtime.NODEJS_10_X || + lambdaFunctionProps.runtime === lambda.Runtime.NODEJS_12_X) { + _lambdaFunctionProps = overrideProps(DefaultLambdaFunctionPropsForNodeJS(lambdaServiceRole), lambdaFunctionProps); + } + + const lambdafunction = new lambda.Function(scope, 'LambdaFunction', _lambdaFunctionProps); + + const cfnLambdafunction = lambdafunction.node.findChild('Resource') as lambda.CfnFunction; + + cfnLambdafunction.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W58', + reason: `Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.` + }] + } + }; + + return lambdafunction; +} diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/s3-bucket-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/s3-bucket-defaults.ts new file mode 100644 index 000000000..cc57e5ed3 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/s3-bucket-defaults.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as s3 from '@aws-cdk/aws-s3'; +import { RemovalPolicy } from '@aws-cdk/core'; +import { Bucket, BucketProps } from '@aws-cdk/aws-s3'; + +export function DefaultS3Props(loggingBucket ?: Bucket): s3.BucketProps { + if (loggingBucket) { + return { + encryption: s3.BucketEncryption.S3_MANAGED, + versioned: true, + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + removalPolicy: RemovalPolicy.RETAIN, + serverAccessLogsBucket: loggingBucket + } as BucketProps; + } else { + return { + encryption: s3.BucketEncryption.S3_MANAGED, + versioned: true, + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + removalPolicy: RemovalPolicy.RETAIN + } as BucketProps; + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/s3-bucket-helper.ts b/source/patterns/@aws-solutions-konstruk/core/lib/s3-bucket-helper.ts new file mode 100644 index 000000000..4a01a1df7 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/s3-bucket-helper.ts @@ -0,0 +1,98 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import { DefaultS3Props } from './s3-bucket-defaults'; +import { overrideProps } from './utils'; + +export interface BuildS3BucketProps { + /** + * Whether to create a S3 Bucket or use an existing S3 Bucket. + * If set to false, you must provide S3 Bucket as `existingBucketObj` + * + * @default - true + */ + readonly deployBucket?: boolean, + /** + * Existing instance of S3 Bucket object. + * If `deployBucket` is set to false only then this property is required + * + * @default - None + */ + readonly existingBucketObj?: s3.Bucket, + /** + * Optional user provided props to override the default props. + * If `deploy` is set to true only then this property is required + * + * @default - Default props are used + */ + readonly bucketProps?: s3.BucketProps +} + +export function buildS3Bucket(scope: cdk.Construct, props: BuildS3BucketProps): s3.Bucket { + // Conditional s3 Bucket creation + // If deployBucket == false + if (props.hasOwnProperty('deployBucket') && props.deployBucket === false) { + if (props.existingBucketObj) { + return props.existingBucketObj; + } else { + throw Error('Missing existingBucketObj from props for deployBucket = false'); + } + // If deploy == true + } else { + if (props.bucketProps) { + return s3BucketWithLogging(scope, props.bucketProps); + } else { + return s3BucketWithLogging(scope, DefaultS3Props()); + } + } +} + +function s3BucketWithLogging(scope: cdk.Construct, s3BucketProps?: s3.BucketProps): s3.Bucket { + + // Create the Application Bucket + let bucketprops; + + if (s3BucketProps?.serverAccessLogsBucket) { + bucketprops = DefaultS3Props; + } else { + // Create the Logging Bucket + const loggingBucket: s3.Bucket = new s3.Bucket(scope, 'S3LoggingBucket', DefaultS3Props()); + + // Extract the CfnBucket from the loggingBucket + const loggingBucketResource = loggingBucket.node.findChild('Resource') as s3.CfnBucket; + + // Override accessControl configuration and add metadata for the logging bucket + loggingBucketResource.addPropertyOverride('AccessControl', 'LogDeliveryWrite'); + loggingBucketResource.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W35', + reason: `This S3 bucket is used as the access logging bucket for another bucket` + }, { + id: 'W51', + reason: `This S3 bucket Bucket does not need a bucket policy` + }] + } + }; + bucketprops = DefaultS3Props(loggingBucket); + } + + if (s3BucketProps) { + bucketprops = overrideProps(bucketprops, s3BucketProps); + } + const s3Bucket: s3.Bucket = new s3.Bucket(scope, 'S3Bucket', bucketprops); + + return s3Bucket; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/sns-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/sns-defaults.ts new file mode 100644 index 000000000..fd88fbf50 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/sns-defaults.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as sns from '@aws-cdk/aws-sns'; + +const DefaultSnsTopicProps: sns.TopicProps = {}; + +export { DefaultSnsTopicProps }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/sns-helper.ts b/source/patterns/@aws-solutions-konstruk/core/lib/sns-helper.ts new file mode 100644 index 000000000..0056bc5ac --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/sns-helper.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import * as sns from '@aws-cdk/aws-sns'; +import * as kms from '@aws-cdk/aws-kms'; +import { DefaultSnsTopicProps } from './sns-defaults'; +import { buildEncryptionKey } from './kms-helper'; +import * as cdk from '@aws-cdk/core'; +import { overrideProps } from './utils'; + +export interface BuildTopicProps { + /** + * Optional user provided props to override the default props for the SNS topic. + * + * @default - Default props are used. + */ + readonly topicProps?: sns.TopicProps | any + /** + * Use a KMS Key, either managed by this CDK app, or imported. If importing an encryption key, it must be specified in + * the encryptionKey property for this construct. + * + * @default - true (encryption enabled, managed by this CDK app). + */ + readonly enableEncryption?: boolean + /** + * An optional, imported encryption key to encrypt the SNS topic with. + * + * @default - not specified. + */ + readonly encryptionKey?: kms.Key +} + +export function buildTopic(scope: cdk.Construct, props?: BuildTopicProps): sns.Topic { + // If props is undefined, define it + props = (props === undefined) ? {} : props; + // Setup the topic properties + let snsTopicProps; + if (props.hasOwnProperty('topicProps')) { + // If property overrides have been provided, incorporate them and deploy + snsTopicProps = overrideProps(DefaultSnsTopicProps, props.topicProps); + } else { + // If no property overrides, deploy using the default configuration + snsTopicProps = DefaultSnsTopicProps; + } + // Set encryption properties + if (!props.enableEncryption || props.enableEncryption === true) { + if (props.encryptionKey) { + snsTopicProps.masterKey = props.encryptionKey; + } else { + snsTopicProps.masterKey = buildEncryptionKey(scope); + } + } + // Create the stream and return + return new sns.Topic(scope, 'SnsTopic', snsTopicProps); +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/sqs-defaults.ts b/source/patterns/@aws-solutions-konstruk/core/lib/sqs-defaults.ts new file mode 100644 index 000000000..9df6b1f21 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/sqs-defaults.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as sqs from '@aws-cdk/aws-sqs'; +import * as kms from '@aws-cdk/aws-kms'; + +export function DefaultQueueProps(_encryptionMasterKey?: kms.Key) { + const _DefaultQueueProps: sqs.QueueProps = { + encryption: sqs.QueueEncryption.KMS, + encryptionMasterKey: _encryptionMasterKey + }; + return _DefaultQueueProps; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/sqs-helper.ts b/source/patterns/@aws-solutions-konstruk/core/lib/sqs-helper.ts new file mode 100644 index 000000000..b22d454e5 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/sqs-helper.ts @@ -0,0 +1,84 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import * as sqs from '@aws-cdk/aws-sqs'; +import * as kms from '@aws-cdk/aws-kms'; +import * as defaults from './sqs-defaults'; +import * as cdk from '@aws-cdk/core'; +import { overrideProps } from './utils'; + +export interface BuildQueueProps { + /** + * Optional external encryption key to use for stream encryption. + * + * @default - Default props are used. + */ + readonly encryptionKey?: kms.Key + /** + * Optional user provided props to override the default props for the primary queue. + * + * @default - Default props are used. + */ + readonly queueProps?: sqs.QueueProps | any + /** + * Optional dead letter queue to pass bad requests to after the max receive count is reached. + * + * @default - Default props are used. + */ + readonly deadLetterQueue?: sqs.DeadLetterQueue +} + +export function buildQueue(scope: cdk.Construct, id: string, props?: BuildQueueProps): sqs.Queue { + // If props is undefined, define it + props = (props === undefined) ? {} : props; + // Setup the queue + let queueProps; + if (props.queueProps) { + // If property overrides have been provided, incorporate them and deploy + queueProps = overrideProps(defaults.DefaultQueueProps(props.encryptionKey), props.queueProps); + } else { + // If no property overrides, deploy using the default configuration + queueProps = defaults.DefaultQueueProps(props.encryptionKey); + } + // Determine whether a DLQ property should be added + if (props.deadLetterQueue) { + queueProps.deadLetterQueue = props.deadLetterQueue; + } + // Return the queue + return new sqs.Queue(scope, id, queueProps); +} + +export interface BuildDeadLetterQueueProps { + /** + * An existing queue that has already been defined to be used as the dead letter queue. + * + * @default - Default props are used. + */ + readonly deadLetterQueue: sqs.Queue + /** + * The number of times a message can be unsuccesfully dequeued before being moved to the dead-letter queue. + * + * @default - Default props are used + */ + readonly maxReceiveCount: number +} + +export function buildDeadLetterQueue(props: BuildDeadLetterQueueProps): sqs.DeadLetterQueue { + // Setup the queue interface and return + const dlq: sqs.DeadLetterQueue = { + maxReceiveCount: props.maxReceiveCount, + queue: props.deadLetterQueue + }; + return dlq; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/lib/utils.ts b/source/patterns/@aws-solutions-konstruk/core/lib/utils.ts new file mode 100644 index 000000000..5dd07a3a7 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/lib/utils.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as deepmerge from 'deepmerge'; + +function isObject(val: object) { + return val != null && typeof val === 'object' + && Object.prototype.toString.call(val) === '[object Object]'; +} + +function isPlainObject(o: object) { + if (Array.isArray(o) === true) { + return true; + } + + if (isObject(o) === false) { + return false; + } + + // If has modified constructor + const ctor = o.constructor; + if (typeof ctor !== 'function') { + return false; + } + + // If has modified prototype + const prot = ctor.prototype; + if (isObject(prot) === false) { + return false; + } + + // If constructor does not have an Object-specific method + if (prot.hasOwnProperty('isPrototypeOf') === false) { + return false; + } + + // Most likely a plain Object + return true; +} + +function combineMerge(target: any[], source: any[]) { + return target.concat(source); +} + +function overwriteMerge(target: any[], source: any[]) { + target = source; + return target; +} + +export function overrideProps(DefaultProps: object, userProps: object, concatArray: boolean = false): any { + // Override the sensible defaults with user provided props + + if (concatArray) { + return deepmerge(DefaultProps, userProps, { + arrayMerge: combineMerge, + isMergeableObject: isPlainObject + }); + } else { + return deepmerge(DefaultProps, userProps, { + arrayMerge: overwriteMerge, + isMergeableObject: isPlainObject + }); + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/package.json b/source/patterns/@aws-solutions-konstruk/core/package.json new file mode 100644 index 000000000..bc9a3465e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/package.json @@ -0,0 +1,112 @@ +{ + "name": "@aws-solutions-konstruk/core", + "version": "0.8.0", + "description": "Core CDK Construct for patterns library", + "main": "index.js", + "types": "index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/patterns/@aws-solutions-konstruk/core" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm test -- -u" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.core", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "core" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk", + "packageId": "Amazon.Konstruk", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-konstruk.core", + "module": "aws_solutions_konstruk.core" + } + } + }, + "dependencies": { + "@aws-cdk/aws-cloudfront": "~1.25.0", + "@aws-cdk/aws-dynamodb": "~1.25.0", + "@aws-cdk/aws-iot": "~1.25.0", + "@aws-cdk/aws-kinesis": "~1.25.0", + "@aws-cdk/aws-kinesisanalytics": "~1.25.0", + "@aws-cdk/aws-kinesisfirehose": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-lambda-event-sources": "~1.25.0", + "@aws-cdk/aws-logs": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-cdk/aws-sns": "~1.25.0", + "@aws-cdk/aws-sqs": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-cdk/aws-kms": "~1.25.0", + "@aws-cdk/aws-events": "~1.25.0", + "@aws-cdk/aws-cognito": "~1.25.0", + "@aws-cdk/aws-elasticsearch": "~1.25.0", + "@aws-cdk/aws-cloudwatch": "~1.25.0", + "deepmerge": "^4.0.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "bundledDependencies": [ + "deepmerge" + ], + "peerDependencies": { + "@aws-cdk/aws-cloudfront": "~1.25.0", + "@aws-cdk/aws-dynamodb": "~1.25.0", + "@aws-cdk/aws-iot": "~1.25.0", + "@aws-cdk/aws-kinesis": "~1.25.0", + "@aws-cdk/aws-kinesisanalytics": "~1.25.0", + "@aws-cdk/aws-kinesisfirehose": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-lambda-event-sources": "~1.25.0", + "@aws-cdk/aws-logs": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-cdk/aws-sns": "~1.25.0", + "@aws-cdk/aws-sqs": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-cdk/aws-kms": "~1.25.0", + "@aws-cdk/aws-events": "~1.25.0", + "@aws-cdk/aws-cognito": "~1.25.0", + "@aws-cdk/aws-elasticsearch": "~1.25.0", + "@aws-cdk/aws-cloudwatch": "~1.25.0" + } +} diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/apigateway-helper.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/apigateway-helper.test.js.snap new file mode 100644 index 000000000..08a773a2f --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/apigateway-helper.test.js.snap @@ -0,0 +1,1128 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test default RestApi deployment w/ ApiGatewayProps 1`] = ` +Object { + "Outputs": Object { + "RestApiEndpoint0551178A": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "RestApi0C43BF4B", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/", + ], + ], + }, + }, + }, + "Resources": Object { + "ApiAccessLogGroupCEA70788": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "LambdaRestApiAccount": Object { + "DependsOn": Array [ + "RestApi0C43BF4B", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "LambdaRestApiCloudWatchRoleF339D4E6": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "RestApi0C43BF4B": Object { + "Properties": Object { + "EndpointConfiguration": Object { + "Types": Array [ + "EDGE", + ], + }, + "Name": "customRestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "RestApiDeployment180EC50321995420db12611041100554516080b6": Object { + "DependsOn": Array [ + "RestApiapigatewayresourcePOST2678115A", + "RestApiapigatewayresource242D19A1", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "RestApiDeploymentStageprod3855DE66": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "ApiAccessLogGroupCEA70788", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "RestApiDeployment180EC50321995420db12611041100554516080b6", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "RestApiUsagePlan6E1C537A": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "Stage": Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "RestApiapigatewayresource242D19A1": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "PathPart": "api-gateway-resource", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "RestApiapigatewayresourcePOST2678115A": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "POST", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "IntegrationResponses": Array [ + Object { + "ResponseTemplates": Object { + "text/html": "Success", + }, + "StatusCode": "200", + }, + Object { + "ResponseTemplates": Object { + "text/html": "Error", + }, + "SelectionPattern": "500", + "StatusCode": "500", + }, + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": Object { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'", + }, + "RequestTemplates": Object { + "application/x-www-form-urlencoded": "Action=SendMessage&MessageBody=$util.urlEncode(\\"$input.body\\")&MessageAttribute.1.Name=queryParam1&MessageAttribute.1.Value.StringValue=$input.params(\\"query_param_1\\")&MessageAttribute.1.Value.DataType=String", + }, + "Type": "AWS", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":sqs:path/12345678/thisqueuequeueName", + ], + ], + }, + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "200", + }, + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "500", + }, + ], + "RequestParameters": Object { + "method.request.querystring.query_param_1": true, + }, + "ResourceId": Object { + "Ref": "RestApiapigatewayresource242D19A1", + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + }, +} +`; + +exports[`Test default RestApi deployment w/o ApiGatewayProps 1`] = ` +Object { + "Outputs": Object { + "RestApiEndpoint0551178A": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "RestApi0C43BF4B", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/", + ], + ], + }, + }, + }, + "Resources": Object { + "ApiAccessLogGroupCEA70788": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "LambdaRestApiAccount": Object { + "DependsOn": Array [ + "RestApi0C43BF4B", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "LambdaRestApiCloudWatchRoleF339D4E6": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "RestApi0C43BF4B": Object { + "Properties": Object { + "EndpointConfiguration": Object { + "Types": Array [ + "EDGE", + ], + }, + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "RestApiDeployment180EC50321995420db12611041100554516080b6": Object { + "DependsOn": Array [ + "RestApiapigatewayresourcePOST2678115A", + "RestApiapigatewayresource242D19A1", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "RestApiDeploymentStageprod3855DE66": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "ApiAccessLogGroupCEA70788", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "RestApiDeployment180EC50321995420db12611041100554516080b6", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "RestApiUsagePlan6E1C537A": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "Stage": Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "RestApiapigatewayresource242D19A1": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "PathPart": "api-gateway-resource", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "RestApiapigatewayresourcePOST2678115A": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "POST", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "IntegrationResponses": Array [ + Object { + "ResponseTemplates": Object { + "text/html": "Success", + }, + "StatusCode": "200", + }, + Object { + "ResponseTemplates": Object { + "text/html": "Error", + }, + "SelectionPattern": "500", + "StatusCode": "500", + }, + ], + "PassthroughBehavior": "NEVER", + "RequestParameters": Object { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'", + }, + "RequestTemplates": Object { + "application/x-www-form-urlencoded": "Action=SendMessage&MessageBody=$util.urlEncode(\\"$input.body\\")&MessageAttribute.1.Name=queryParam1&MessageAttribute.1.Value.StringValue=$input.params(\\"query_param_1\\")&MessageAttribute.1.Value.DataType=String", + }, + "Type": "AWS", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":sqs:path/12345678/thisqueuequeueName", + ], + ], + }, + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "200", + }, + Object { + "ResponseParameters": Object { + "method.response.header.Content-Type": true, + }, + "StatusCode": "500", + }, + ], + "RequestParameters": Object { + "method.request.querystring.query_param_1": true, + }, + "ResourceId": Object { + "Ref": "RestApiapigatewayresource242D19A1", + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + }, +} +`; + +exports[`snapshot test RegionalApiGateway default params 1`] = ` +Object { + "Outputs": Object { + "RestApiEndpoint0551178A": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "RestApi0C43BF4B", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": Object { + "Description": "Artifact hash for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": Object { + "Description": "S3 bucket for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": Object { + "Description": "S3 key for asset version \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + }, + "Resources": Object { + "ApiAccessLogGroupCEA70788": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaRestApiAccount": Object { + "DependsOn": Array [ + "RestApi0C43BF4B", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "LambdaRestApiCloudWatchRoleF339D4E6": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "RestApi0C43BF4B": Object { + "Properties": Object { + "EndpointConfiguration": Object { + "Types": Array [ + "REGIONAL", + ], + }, + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "RestApiANYA7C1DC94": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiANYApiPermissionRestApiANY3A99B4EE": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiANYApiPermissionTestRestApiANY79BD91F2": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8": Object { + "DependsOn": Array [ + "RestApiproxyANY1786B242", + "RestApiproxyC95856DD", + "RestApiANYA7C1DC94", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "RestApiDeploymentStageprod3855DE66": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "ApiAccessLogGroupCEA70788", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "RestApiDeployment180EC503c4d635ab14db0b242903e058a000f3f8", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "RestApiUsagePlan6E1C537A": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "Stage": Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "RestApiproxyANY1786B242": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "RestApiproxyC95856DD", + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiproxyANYApiPermissionRestApiANYproxy9C9912F9": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyANYApiPermissionTestRestApiANYproxyCB7BC56D": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyC95856DD": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/cloudfront-distribution-api-gateway-helper.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/cloudfront-distribution-api-gateway-helper.test.js.snap new file mode 100644 index 000000000..4dea3d1d2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/cloudfront-distribution-api-gateway-helper.test.js.snap @@ -0,0 +1,608 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`cloudfront distribution for ApiGateway with default params 1`] = ` +Object { + "Outputs": Object { + "RestApiEndpoint0551178A": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "RestApi0C43BF4B", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": Object { + "Description": "Artifact hash for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": Object { + "Description": "S3 bucket for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": Object { + "Description": "S3 key for asset version \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + }, + "Resources": Object { + "CloudFrontDistributionCFDistribution599ADCC4": Object { + "Properties": Object { + "DistributionConfig": Object { + "DefaultCacheBehavior": Object { + "AllowedMethods": Array [ + "GET", + "HEAD", + ], + "CachedMethods": Array [ + "GET", + "HEAD", + ], + "Compress": true, + "ForwardedValues": Object { + "Cookies": Object { + "Forward": "none", + }, + "QueryString": false, + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https", + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": Object { + "Bucket": Object { + "Fn::GetAtt": Array [ + "CloudfrontLoggingBucket3C3EFAA7", + "RegionalDomainName", + ], + }, + "IncludeCookies": false, + }, + "Origins": Array [ + Object { + "CustomOriginConfig": Object { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginKeepaliveTimeout": 5, + "OriginProtocolPolicy": "https-only", + "OriginReadTimeout": 30, + "OriginSSLProtocols": Array [ + "TLSv1.2", + ], + }, + "DomainName": Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "/", + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "://", + Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "RestApi0C43BF4B", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/", + ], + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + "Id": "origin1", + }, + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": Object { + "CloudFrontDefaultCertificate": true, + }, + }, + }, + "Type": "AWS::CloudFront::Distribution", + }, + "CloudfrontLoggingBucket3C3EFAA7": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + Object { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleC555A460", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRoleC555A460", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRoleC555A460": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "RestApi0C43BF4B": Object { + "Properties": Object { + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "RestApiANYA7C1DC94": Object { + "Properties": Object { + "AuthorizationType": "NONE", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiANYApiPermissionRestApiANY3A99B4EE": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiANYApiPermissionTestRestApiANY79BD91F2": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiAccount7C83CF5A": Object { + "DependsOn": Array [ + "RestApi0C43BF4B", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "RestApiCloudWatchRoleE3ED6605", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "RestApiCloudWatchRoleE3ED6605": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "RestApiDeployment180EC503d2c6df3c8dc8b7193b98c1a0bff4e677": Object { + "DependsOn": Array [ + "RestApiproxyANY1786B242", + "RestApiproxyC95856DD", + "RestApiANYA7C1DC94", + ], + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "RestApiDeploymentStageprod3855DE66": Object { + "Properties": Object { + "DeploymentId": Object { + "Ref": "RestApiDeployment180EC503d2c6df3c8dc8b7193b98c1a0bff4e677", + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "RestApiproxyANY1786B242": Object { + "Properties": Object { + "AuthorizationType": "NONE", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "RestApiproxyC95856DD", + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiproxyANYApiPermissionRestApiANYproxy9C9912F9": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyANYApiPermissionTestRestApiANYproxyCB7BC56D": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyC95856DD": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/cloudfront-distribution-s3-helper.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/cloudfront-distribution-s3-helper.test.js.snap new file mode 100644 index 000000000..56e065544 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/cloudfront-distribution-s3-helper.test.js.snap @@ -0,0 +1,289 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`cloudfront distribution with default params 1`] = ` +Object { + "Resources": Object { + "CloudFrontDistributionCFDistribution599ADCC4": Object { + "Properties": Object { + "DistributionConfig": Object { + "DefaultCacheBehavior": Object { + "AllowedMethods": Array [ + "GET", + "HEAD", + ], + "CachedMethods": Array [ + "GET", + "HEAD", + ], + "Compress": true, + "ForwardedValues": Object { + "Cookies": Object { + "Forward": "none", + }, + "QueryString": false, + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https", + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": Object { + "Bucket": Object { + "Fn::GetAtt": Array [ + "CloudfrontLoggingBucket3C3EFAA7", + "RegionalDomainName", + ], + }, + "IncludeCookies": false, + }, + "Origins": Array [ + Object { + "DomainName": Object { + "Fn::GetAtt": Array [ + "S3Bucket07682993", + "RegionalDomainName", + ], + }, + "Id": "origin1", + "S3OriginConfig": Object { + "OriginAccessIdentity": Object { + "Fn::Join": Array [ + "", + Array [ + "origin-access-identity/cloudfront/", + Object { + "Ref": "CloudFrontOriginAccessIdentity", + }, + ], + ], + }, + }, + }, + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": Object { + "CloudFrontDefaultCertificate": true, + }, + }, + }, + "Type": "AWS::CloudFront::Distribution", + }, + "CloudFrontOriginAccessIdentity": Object { + "Properties": Object { + "CloudFrontOriginAccessIdentityConfig": Object { + "Comment": "Access S3 bucket content only through CloudFront", + }, + }, + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + }, + "CloudfrontLoggingBucket3C3EFAA7": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + Object { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "S3Bucket07682993": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "S3LoggingBucket800A2B27", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "S3BucketPolicyF560589A": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "F16", + "reason": "Public website bucket policy requires a wildcard principal", + }, + ], + }, + }, + "Properties": Object { + "Bucket": Object { + "Ref": "S3Bucket07682993", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::cloudfront:user/CloudFront Origin Access Identity ", + Object { + "Ref": "CloudFrontOriginAccessIdentity", + }, + ], + ], + }, + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "S3Bucket07682993", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "S3Bucket07682993", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": "s3:GetObject", + "Effect": "Allow", + "Principal": Object { + "CanonicalUser": Object { + "Fn::GetAtt": Array [ + "CloudFrontOriginAccessIdentity", + "S3CanonicalUserId", + ], + }, + }, + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "S3Bucket07682993", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "S3LoggingBucket800A2B27": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/cloudwatch-log-group.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/cloudwatch-log-group.test.js.snap new file mode 100644 index 000000000..9cd7842cb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/cloudwatch-log-group.test.js.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`cw log group with default params 1`] = ` +Object { + "Resources": Object { + "testcwlogsdefault5C05821C": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; + +exports[`cw log group with log group name 1`] = ` +Object { + "Resources": Object { + "testcwlogsdefault5C05821C": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "LogGroupName": "lambda-log-group", + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/congnito-helper.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/congnito-helper.test.js.snap new file mode 100644 index 000000000..373201104 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/congnito-helper.test.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test buildUserPool default params 1`] = ` +Object { + "Resources": Object { + "CognitoUserPool53E37E69": Object { + "Properties": Object { + "LambdaConfig": Object {}, + "UserPoolAddOns": Object { + "AdvancedSecurityMode": "ENFORCED", + }, + }, + "Type": "AWS::Cognito::UserPool", + }, + }, +} +`; + +exports[`snapshot test buildUserPoolClient default params 1`] = ` +Object { + "Resources": Object { + "CognitoUserPool53E37E69": Object { + "Properties": Object { + "LambdaConfig": Object {}, + "UserPoolAddOns": Object { + "AdvancedSecurityMode": "ENFORCED", + }, + }, + "Type": "AWS::Cognito::UserPool", + }, + "CognitoUserPoolClient5AB59AE4": Object { + "Properties": Object { + "UserPoolId": Object { + "Ref": "CognitoUserPool53E37E69", + }, + }, + "Type": "AWS::Cognito::UserPoolClient", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/dynamo-table.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/dynamo-table.test.js.snap new file mode 100644 index 000000000..838238a35 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/dynamo-table.test.js.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test TableProps default params 1`] = ` +Object { + "Resources": Object { + "testdynamodefaults72AF3E8C": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "AttributeDefinitions": Array [ + Object { + "AttributeName": "id", + "AttributeType": "S", + }, + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": Array [ + Object { + "AttributeName": "id", + "KeyType": "HASH", + }, + ], + "SSESpecification": Object { + "SSEEnabled": true, + }, + }, + "Type": "AWS::DynamoDB::Table", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; + +exports[`snapshot test TableWithStream default params 1`] = ` +Object { + "Resources": Object { + "testdynamostreamdefaultsFD08DF32": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "AttributeDefinitions": Array [ + Object { + "AttributeName": "id", + "AttributeType": "S", + }, + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": Array [ + Object { + "AttributeName": "id", + "KeyType": "HASH", + }, + ], + "SSESpecification": Object { + "SSEEnabled": true, + }, + "StreamSpecification": Object { + "StreamViewType": "NEW_AND_OLD_IMAGES", + }, + }, + "Type": "AWS::DynamoDB::Table", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/elasticsearch-helper.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/elasticsearch-helper.test.js.snap new file mode 100644 index 000000000..958551ade --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/elasticsearch-helper.test.js.snap @@ -0,0 +1,326 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test buildElasticSearch default params 1`] = ` +Object { + "Resources": Object { + "CognitoAuthorizedRole14E74FE0": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": Object { + "ForAnyValue:StringLike": Object { + "cognito-identity.amazonaws.com:amr": "authenticated", + }, + "StringEquals": Object { + "cognito-identity.amazonaws.com:aud": Object { + "Ref": "CognitoIdentityPool", + }, + }, + }, + "Effect": "Allow", + "Principal": Object { + "Federated": "cognito-identity.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:es:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":domain/test-domain/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CognitoAccessPolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "CognitoIdentityPool": Object { + "Properties": Object { + "AllowUnauthenticatedIdentities": false, + "CognitoIdentityProviders": Array [ + Object { + "ClientId": Object { + "Ref": "CognitoUserPoolClient5AB59AE4", + }, + "ProviderName": Object { + "Fn::GetAtt": Array [ + "CognitoUserPool53E37E69", + "ProviderName", + ], + }, + "ServerSideTokenCheck": true, + }, + ], + }, + "Type": "AWS::Cognito::IdentityPool", + }, + "CognitoKibanaConfigureRole62CCE76A": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "es.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "CognitoKibanaConfigureRolePolicy76F46A5E": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateElasticsearchDomainConfig", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "CognitoUserPool53E37E69", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:cognito-identity:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":identitypool/", + Object { + "Ref": "CognitoIdentityPool", + }, + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:es:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":domain/test-domain", + ], + ], + }, + ], + }, + Object { + "Action": "iam:PassRole", + "Condition": Object { + "StringLike": Object { + "iam:PassedToService": "cognito-identity.amazonaws.com", + }, + }, + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CognitoKibanaConfigureRolePolicy76F46A5E", + "Roles": Array [ + Object { + "Ref": "CognitoKibanaConfigureRole62CCE76A", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "CognitoUserPool53E37E69": Object { + "Properties": Object { + "LambdaConfig": Object {}, + "UserPoolAddOns": Object { + "AdvancedSecurityMode": "ENFORCED", + }, + }, + "Type": "AWS::Cognito::UserPool", + }, + "CognitoUserPoolClient5AB59AE4": Object { + "Properties": Object { + "ClientName": "test", + "UserPoolId": Object { + "Ref": "CognitoUserPool53E37E69", + }, + }, + "Type": "AWS::Cognito::UserPoolClient", + }, + "ElasticsearchDomain": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W28", + "reason": "The ES Domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific ES instance only", + }, + ], + }, + }, + "Properties": Object { + "AccessPolicies": Object { + "Statement": Array [ + Object { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::GetAtt": Array [ + "CognitoAuthorizedRole14E74FE0", + "Arn", + ], + }, + }, + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:es:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":domain/test-domain/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "CognitoOptions": Object { + "Enabled": true, + "IdentityPoolId": Object { + "Ref": "CognitoIdentityPool", + }, + "RoleArn": Object { + "Fn::GetAtt": Array [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn", + ], + }, + "UserPoolId": Object { + "Ref": "CognitoUserPool53E37E69", + }, + }, + "DomainName": "test-domain", + "EBSOptions": Object { + "EBSEnabled": true, + "VolumeSize": 10, + }, + "ElasticsearchClusterConfig": Object { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "InstanceCount": 3, + "ZoneAwarenessConfig": Object { + "AvailabilityZoneCount": 3, + }, + "ZoneAwarenessEnabled": true, + }, + "ElasticsearchVersion": "6.3", + "EncryptionAtRestOptions": Object { + "Enabled": true, + }, + "NodeToNodeEncryptionOptions": Object { + "Enabled": true, + }, + "SnapshotOptions": Object { + "AutomatedSnapshotStartHour": 1, + }, + }, + "Type": "AWS::Elasticsearch::Domain", + }, + "IdentityPoolRoleMapping": Object { + "Properties": Object { + "IdentityPoolId": Object { + "Ref": "CognitoIdentityPool", + }, + "Roles": Object { + "authenticated": Object { + "Fn::GetAtt": Array [ + "CognitoAuthorizedRole14E74FE0", + "Arn", + ], + }, + }, + }, + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + }, + "UserPoolDomain": Object { + "DependsOn": Array [ + "CognitoUserPool53E37E69", + ], + "Properties": Object { + "Domain": "test-domain", + "UserPoolId": Object { + "Ref": "CognitoUserPool53E37E69", + }, + }, + "Type": "AWS::Cognito::UserPoolDomain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/events-rule.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/events-rule.test.js.snap new file mode 100644 index 000000000..fff00aaeb --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/events-rule.test.js.snap @@ -0,0 +1,160 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test EventsRuleProps default params 1`] = ` +Object { + "Parameters": Object { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": Object { + "Description": "Artifact hash for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": Object { + "Description": "S3 bucket for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": Object { + "Description": "S3 key for asset version \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + }, + "Resources": Object { + "EventsD32975C2": Object { + "Properties": Object { + "ScheduleExpression": "rate(5 minutes)", + "State": "ENABLED", + "Targets": Array [ + Object { + "Arn": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Id": "Target0", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/iot-rule.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/iot-rule.test.js.snap new file mode 100644 index 000000000..749c4ad7d --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/iot-rule.test.js.snap @@ -0,0 +1,128 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test TopicRuleProps default params 1`] = ` +Object { + "Parameters": Object { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": Object { + "Description": "Artifact hash for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": Object { + "Description": "S3 bucket for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": Object { + "Description": "S3 key for asset version \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + }, + "Resources": Object { + "IotTopic": Object { + "Properties": Object { + "TopicRulePayload": Object { + "Actions": Array [ + Object { + "Lambda": Object { + "FunctionArn": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + }, + }, + ], + "RuleDisabled": false, + "Sql": "SELECT * FROM 'topic/#'", + }, + }, + "Type": "AWS::IoT::TopicRule", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleC555A460", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRoleC555A460", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRoleC555A460": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-analytics-helper.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-analytics-helper.test.js.snap new file mode 100644 index 000000000..bc64ef78a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-analytics-helper.test.js.snap @@ -0,0 +1,112 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test default functionality 1`] = ` +Object { + "Resources": Object { + "KinesisAnalytics": Object { + "DependsOn": Array [ + "KinesisAnalyticsPolicy88FFA7CD", + ], + "Properties": Object { + "Inputs": Array [ + Object { + "InputSchema": Object { + "RecordColumns": Array [ + Object { + "Mapping": "$.ticker_symbol", + "Name": "ticker_symbol", + "SqlType": "VARCHAR(4)", + }, + Object { + "Mapping": "$.sector", + "Name": "sector", + "SqlType": "VARCHAR(16)", + }, + Object { + "Mapping": "$.change", + "Name": "change", + "SqlType": "REAL", + }, + Object { + "Mapping": "$.price", + "Name": "price", + "SqlType": "REAL", + }, + ], + "RecordEncoding": "UTF-8", + "RecordFormat": Object { + "RecordFormatType": "JSON", + }, + }, + "KinesisFirehoseInput": Object { + "ResourceARN": Object { + "Fn::GetAtt": Array [ + "KinesisFirehose", + "Arn", + ], + }, + "RoleARN": Object { + "Fn::GetAtt": Array [ + "KinesisAnalyticsRoleCBFE2DD3", + "Arn", + ], + }, + }, + "NamePrefix": "SOURCE_SQL_STREAM", + }, + ], + }, + "Type": "AWS::KinesisAnalytics::Application", + }, + "KinesisAnalyticsPolicy88FFA7CD": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "firehose:DescribeDeliveryStream", + "firehose:Get*", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "KinesisFirehose", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "KinesisAnalyticsPolicy88FFA7CD", + "Roles": Array [ + Object { + "Ref": "KinesisAnalyticsRoleCBFE2DD3", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "KinesisAnalyticsRoleCBFE2DD3": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "kinesisanalytics.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "KinesisFirehose": Object { + "Type": "AWS::KinesisFirehose::DeliveryStream", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-analytics.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-analytics.test.js.snap new file mode 100644 index 000000000..a34b3b7b2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-analytics.test.js.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test kinesisanalytics default params 1`] = ` +Object { + "Resources": Object { + "KinesisAnalytics": Object { + "Properties": Object { + "Inputs": Array [], + }, + "Type": "AWS::KinesisAnalytics::Application", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-firehose-s3-defaults.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-firehose-s3-defaults.test.js.snap new file mode 100644 index 000000000..bc3b0eeab --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-firehose-s3-defaults.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test kinesisfirehose default params 1`] = ` +Object { + "Resources": Object { + "KinesisFirehose": Object { + "Properties": Object { + "ExtendedS3DestinationConfiguration": Object { + "BucketARN": "bucket_arn", + "BufferingHints": Object { + "IntervalInSeconds": 300, + "SizeInMBs": 5, + }, + "CloudWatchLoggingOptions": Object { + "Enabled": true, + "LogGroupName": "log_group", + "LogStreamName": "log_stream", + }, + "CompressionFormat": "GZIP", + "RoleARN": "role_arn", + }, + }, + "Type": "AWS::KinesisFirehose::DeliveryStream", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-streams-defaults.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-streams-defaults.test.js.snap new file mode 100644 index 000000000..16e052c13 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-streams-defaults.test.js.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test kinesisstream default params 1`] = ` +Object { + "Resources": Object { + "KinesisStream46752A3E": Object { + "Properties": Object { + "RetentionPeriodHours": 24, + "ShardCount": 1, + "StreamEncryption": Object { + "EncryptionType": "KMS", + "KeyId": Object { + "Fn::GetAtt": Array [ + "KinesisStreamKey72E22A02", + "Arn", + ], + }, + }, + }, + "Type": "AWS::Kinesis::Stream", + }, + "KinesisStreamKey72E22A02": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "Description": "Created by KinesisStream", + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-streams-helper.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-streams-helper.test.js.snap new file mode 100644 index 000000000..add414770 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kinesis-streams-helper.test.js.snap @@ -0,0 +1,152 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test deployment w/ custom properties 1`] = ` +Object { + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "KinesisStream46752A3E": Object { + "Properties": Object { + "Name": "myCustomKinesisStream", + "RetentionPeriodHours": 24, + "ShardCount": 1, + "StreamEncryption": Object { + "EncryptionType": "KMS", + "KeyId": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + }, + "Type": "AWS::Kinesis::Stream", + }, + }, +} +`; + +exports[`Test minimal deployment with no properties 1`] = ` +Object { + "Resources": Object { + "KinesisStream46752A3E": Object { + "Properties": Object { + "RetentionPeriodHours": 24, + "ShardCount": 1, + "StreamEncryption": Object { + "EncryptionType": "KMS", + "KeyId": Object { + "Fn::GetAtt": Array [ + "KinesisStreamKey72E22A02", + "Arn", + ], + }, + }, + }, + "Type": "AWS::Kinesis::Stream", + }, + "KinesisStreamKey72E22A02": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "Description": "Created by KinesisStream", + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kms-helper.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kms-helper.test.js.snap new file mode 100644 index 000000000..ce4625cb1 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/kms-helper.test.js.snap @@ -0,0 +1,119 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test minimal deployment with no properties 1`] = ` +Object { + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; + +exports[`Test minimal deployment with no properties 2`] = ` +Object { + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": false, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/lambda-func.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/lambda-func.test.js.snap new file mode 100644 index 000000000..b05adba85 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/lambda-func.test.js.snap @@ -0,0 +1,142 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot test LambdaFunction default params 1`] = ` +Object { + "Parameters": Object { + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cArtifactHash00A70A91": Object { + "Description": "Artifact hash for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC": Object { + "Description": "S3 bucket for asset \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872": Object { + "Description": "S3 key for asset version \\"42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c\\"", + "Type": "String", + }, + }, + "Resources": Object { + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3Bucket1F467BCC", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198cS3VersionKey9E4F7872", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/s3-bucket-helper.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/s3-bucket-helper.test.js.snap new file mode 100644 index 000000000..4ae376bcc --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/s3-bucket-helper.test.js.snap @@ -0,0 +1,168 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`s3 bucket with default params 1`] = ` +Object { + "Resources": Object { + "S3Bucket07682993": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "S3LoggingBucket800A2B27", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "S3LoggingBucket800A2B27": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; + +exports[`s3 bucket with default params and bucket names 1`] = ` +Object { + "Resources": Object { + "S3Bucket07682993": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "BucketName": "my-bucket", + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "S3LoggingBucket800A2B27", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "S3LoggingBucket800A2B27": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; + +exports[`s3 bucket with existingBucketObj 1`] = ` +Object { + "Resources": Object { + "mybucket15D133BF": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/s3-bucket.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/s3-bucket.test.js.snap new file mode 100644 index 000000000..7d35947e6 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/s3-bucket.test.js.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`s3 bucket with default params 1`] = ` +Object { + "Resources": Object { + "tests3defaults80430774": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/sns-helper.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/sns-helper.test.js.snap new file mode 100644 index 000000000..de1aafec9 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/sns-helper.test.js.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test deployment w/ custom properties 1`] = ` +Object { + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "SnsTopic2C1570A4": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Ref": "EncryptionKey1B843E66", + }, + "TopicName": "custom-topic", + }, + "Type": "AWS::SNS::Topic", + }, + }, +} +`; + +exports[`Test deployment w/ imported encryption key 1`] = ` +Object { + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "SnsTopic2C1570A4": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Ref": "EncryptionKey1B843E66", + }, + "TopicName": "custom-topic", + }, + "Type": "AWS::SNS::Topic", + }, + }, +} +`; + +exports[`Test deployment with no properties 1`] = ` +Object { + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "SnsTopic2C1570A4": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Ref": "EncryptionKey1B843E66", + }, + }, + "Type": "AWS::SNS::Topic", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/sqs-helper.test.js.snap b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/sqs-helper.test.js.snap new file mode 100644 index 000000000..21679a006 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/__snapshots__/sqs-helper.test.js.snap @@ -0,0 +1,283 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test dead letter queue deployment/configuration 1`] = ` +Object { + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "deadletterqueueD1EEB012": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::GetAtt": Array [ + "deadletterqueueKey123D45B8", + "Arn", + ], + }, + }, + "Type": "AWS::SQS::Queue", + }, + "deadletterqueueKey123D45B8": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "Description": "Created by dead-letter-queue", + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "primaryqueue045A5712": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + "RedrivePolicy": Object { + "deadLetterTargetArn": Object { + "Fn::GetAtt": Array [ + "deadletterqueueD1EEB012", + "Arn", + ], + }, + "maxReceiveCount": 3, + }, + }, + "Type": "AWS::SQS::Queue", + }, + }, +} +`; + +exports[`Test deployment w/ custom properties 1`] = ` +Object { + "Resources": Object { + "EncryptionKey1B843E66": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "EnableKeyRotation": true, + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "primaryqueue045A5712": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::GetAtt": Array [ + "EncryptionKey1B843E66", + "Arn", + ], + }, + }, + "Type": "AWS::SQS::Queue", + }, + }, +} +`; + +exports[`Test minimal deployment with no properties 1`] = ` +Object { + "Resources": Object { + "primaryqueue045A5712": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::GetAtt": Array [ + "primaryqueueKeyD3CAD16D", + "Arn", + ], + }, + }, + "Type": "AWS::SQS::Queue", + }, + "primaryqueueKeyD3CAD16D": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "Description": "Created by primary-queue", + "KeyPolicy": Object { + "Statement": Array [ + Object { + "Action": Array [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/patterns/@aws-solutions-konstruk/core/test/apigateway-helper.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/apigateway-helper.test.ts new file mode 100644 index 000000000..a0ac15c63 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/apigateway-helper.test.ts @@ -0,0 +1,217 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils, ResourcePart } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as api from '@aws-cdk/aws-apigateway'; +import * as defaults from '../index'; +import '@aws-cdk/assert/jest'; + +function deployRegionalApiGateway(stack: Stack) { + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + const fn = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + + return defaults.RegionalLambdaRestApi(stack, fn); +} + +test('snapshot test RegionalApiGateway default params', () => { + const stack = new Stack(); + deployRegionalApiGateway(stack); + + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('Test override for RegionalApiGateway', () => { + const stack = new Stack(); + + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + const fn = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + + defaults.RegionalLambdaRestApi(stack, fn, { + handler: fn, + description: 'Hello World' + }); + + expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Type: "AWS::ApiGateway::RestApi", + Properties: { + Description: "Hello World", + EndpointConfiguration: { + Types: [ + "REGIONAL" + ] + }, + Name: "RestApi" + } + }, ResourcePart.CompleteDefinition); +}); + +test('Test override for GlobalApiGateway', () => { + const stack = new Stack(); + + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + const fn = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + + defaults.GlobalLambdaRestApi(stack, fn, { + handler: fn, + restApiName: "HelloWorld" + }); + + expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Type: "AWS::ApiGateway::RestApi", + Properties: { + EndpointConfiguration: { + Types: [ + "EDGE" + ] + }, + Name: "HelloWorld" + } + }, ResourcePart.CompleteDefinition); +}); + +test('Test ApiGateway::Account resource for RegionalApiGateway', () => { + const stack = new Stack(); + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + const fn = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + + defaults.RegionalLambdaRestApi(stack, fn); + + expect(stack).toHaveResource('AWS::ApiGateway::Account', { + CloudWatchRoleArn: { + "Fn::GetAtt": [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn" + ] + } + }); +}); + +test('Test ApiGateway::Account resource for GlobalApiGateway', () => { + const stack = new Stack(); + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + const fn = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + + defaults.GlobalLambdaRestApi(stack, fn); + + expect(stack).toHaveResource('AWS::ApiGateway::Account', { + CloudWatchRoleArn: { + "Fn::GetAtt": [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn" + ] + } + }); +}); + +test('Test default RestApi deployment w/o ApiGatewayProps', () => { + const stack = new Stack(); + setupRestApi(stack); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('Test default RestApi deployment w/ ApiGatewayProps', () => { + const stack = new Stack(); + setupRestApi(stack, { + restApiName: "customRestApi" + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // Assertion 2 + expect(stack).toHaveResource('AWS::ApiGateway::RestApi', { + Name: "customRestApi" + }); +}); + +function setupRestApi(stack: Stack, apiProps?: any): void { + const restApi = defaults.GlobalRestApi(stack, apiProps); + // Setup the API Gateway resource + const apiGatewayResource = restApi.root.addResource('api-gateway-resource'); + // Setup the API Gateway Integration + const apiGatewayIntegration = new api.AwsIntegration({ + service: "sqs", + integrationHttpMethod: "POST", + options: { + passthroughBehavior: api.PassthroughBehavior.NEVER, + requestParameters: { + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" + }, + requestTemplates: { + "application/x-www-form-urlencoded": "Action=SendMessage&MessageBody=$util.urlEncode(\"$input.body\")&MessageAttribute.1.Name=queryParam1&MessageAttribute.1.Value.StringValue=$input.params(\"query_param_1\")&MessageAttribute.1.Value.DataType=String" + }, + integrationResponses: [ + { + statusCode: "200", + responseTemplates: { + "text/html": "Success" + } + }, + { + statusCode: "500", + responseTemplates: { + "text/html": "Error" + }, + selectionPattern: "500" + } + ] + }, + path: '12345678' + "/" + 'thisqueuequeueName' + }); + // Setup the API Gateway method(s) + apiGatewayResource.addMethod('POST', apiGatewayIntegration, { + requestParameters: { + "method.request.querystring.query_param_1": true + }, + methodResponses: [ + { + statusCode: "200", + responseParameters: { + "method.response.header.Content-Type": true + } + }, + { + statusCode: "500", + responseParameters: { + "method.response.header.Content-Type": true + }, + } + ] + }); +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/cloudfront-distribution-api-gateway-helper.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/cloudfront-distribution-api-gateway-helper.test.ts new file mode 100644 index 000000000..f1b0f2e3a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/cloudfront-distribution-api-gateway-helper.test.ts @@ -0,0 +1,284 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as api from '@aws-cdk/aws-apigateway'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as defaults from '../index'; +import * as s3 from '@aws-cdk/aws-s3'; +import { CloudFrontDistributionForApiGateway } from '../lib/cloudfront-distribution-helper'; +import '@aws-cdk/assert/jest'; +import { OriginProtocolPolicy } from '@aws-cdk/aws-cloudfront'; + +test('cloudfront distribution for ApiGateway with default params', () => { + const stack = new Stack(); + + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + const func = new lambda.Function(stack, 'LambdaFunction', lambdaFunctionProps); + const _api = new api.LambdaRestApi(stack, 'RestApi', { + handler: func + }); + CloudFrontDistributionForApiGateway(stack, _api); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('test cloudfront for Api Gateway with user provided logging bucket', () => { + const stack = new Stack(); + + const loggingBucket: s3.Bucket = new s3.Bucket(stack, 'MyCloudfrontLoggingBucket', defaults.DefaultS3Props()); + + const inProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda-test`), + runtime: lambda.Runtime.PYTHON_3_6, + handler: 'index.handler' + }; + + const cfdProps = { + loggingConfig: { + bucket: loggingBucket + } + }; + + const func = defaults.deployLambdaFunction(stack, inProps); + + const _api = new api.LambdaRestApi(stack, 'RestApi1', { + handler: func + }); + + CloudFrontDistributionForApiGateway(stack, _api, cfdProps); + expect(stack).toHaveResourceLike("AWS::CloudFront::Distribution", { + DistributionConfig: { + DefaultCacheBehavior: { + AllowedMethods: [ + "GET", + "HEAD" + ], + CachedMethods: [ + "GET", + "HEAD" + ], + Compress: true, + ForwardedValues: { + Cookies: { + Forward: "none" + }, + QueryString: false + }, + TargetOriginId: "origin1", + ViewerProtocolPolicy: "redirect-to-https" + }, + DefaultRootObject: "index.html", + Enabled: true, + HttpVersion: "http2", + IPV6Enabled: true, + Logging: { + Bucket: { + "Fn::GetAtt": [ + "MyCloudfrontLoggingBucket9AA652E8", + "RegionalDomainName" + ] + }, + IncludeCookies: false + }, + Origins: [ + { + CustomOriginConfig: { + HTTPPort: 80, + HTTPSPort: 443, + OriginKeepaliveTimeout: 5, + OriginProtocolPolicy: "https-only", + OriginReadTimeout: 30, + OriginSSLProtocols: [ + "TLSv1.2" + ] + }, + DomainName: { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "://", + { + "Fn::Join": [ + "", + [ + "https://", + { + Ref: "RestApi1480AC499" + }, + ".execute-api.", + { + Ref: "AWS::Region" + }, + ".", + { + Ref: "AWS::URLSuffix" + }, + "/", + { + Ref: "RestApi1DeploymentStageprod4FFC9BB4" + }, + "/" + ] + ] + } + ] + } + ] + } + ] + } + ] + }, + Id: "origin1" + } + ], + PriceClass: "PriceClass_100", + ViewerCertificate: { + CloudFrontDefaultCertificate: true + } + } + }); +}); + +test('test cloudfront for Api Gateway override properties', () => { + const stack = new Stack(); + + const inProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda-test`), + runtime: lambda.Runtime.PYTHON_3_6, + handler: 'index.handler' + }; + + const func = defaults.deployLambdaFunction(stack, inProps); + + const _api = new api.LambdaRestApi(stack, 'RestApi1', { + handler: func + }); + + const props: cloudfront.CloudFrontWebDistributionProps = { + originConfigs: [ { + customOriginSource: { + domainName: _api.url, + originProtocolPolicy: OriginProtocolPolicy.HTTP_ONLY + }, + behaviors: [ { + isDefaultBehavior: true, + allowedMethods: cloudfront.CloudFrontAllowedMethods.ALL, + cachedMethods: cloudfront.CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS + } ] + } ] + }; + + CloudFrontDistributionForApiGateway(stack, _api, props); + + expect(stack).toHaveResourceLike("AWS::CloudFront::Distribution", { + DistributionConfig: { + DefaultCacheBehavior: { + AllowedMethods: [ + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT" + ], + CachedMethods: [ + "GET", + "HEAD", + "OPTIONS" + ], + Compress: true, + ForwardedValues: { + Cookies: { + Forward: "none" + }, + QueryString: false + }, + TargetOriginId: "origin1", + ViewerProtocolPolicy: "redirect-to-https" + }, + DefaultRootObject: "index.html", + Enabled: true, + HttpVersion: "http2", + IPV6Enabled: true, + Logging: { + Bucket: { + "Fn::GetAtt": [ + "CloudfrontLoggingBucket3C3EFAA7", + "RegionalDomainName" + ] + }, + IncludeCookies: false + }, + Origins: [ + { + CustomOriginConfig: { + HTTPPort: 80, + HTTPSPort: 443, + OriginKeepaliveTimeout: 5, + OriginProtocolPolicy: "http-only", + OriginReadTimeout: 30, + OriginSSLProtocols: [ + "TLSv1.2" + ] + }, + DomainName: { + "Fn::Join": [ + "", + [ + "https://", + { + Ref: "RestApi1480AC499" + }, + ".execute-api.", + { + Ref: "AWS::Region" + }, + ".", + { + Ref: "AWS::URLSuffix" + }, + "/", + { + Ref: "RestApi1DeploymentStageprod4FFC9BB4" + }, + "/" + ] + ] + }, + Id: "origin1" + } + ], + PriceClass: "PriceClass_100", + ViewerCertificate: { + CloudFrontDefaultCertificate: true + } + } + }); + +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/cloudfront-distribution-s3-helper.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/cloudfront-distribution-s3-helper.test.ts new file mode 100644 index 000000000..8f08f8dee --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/cloudfront-distribution-s3-helper.test.ts @@ -0,0 +1,267 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils, ResourcePart } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import { CloudFrontDistributionForS3 } from '../lib/cloudfront-distribution-helper'; +import { buildS3Bucket } from '../lib/s3-bucket-helper'; +import '@aws-cdk/assert/jest'; + +test('cloudfront distribution with default params', () => { + const stack = new Stack(); + const sourceBucket = buildS3Bucket(stack, { + deployBucket: true + }); + CloudFrontDistributionForS3(stack, sourceBucket); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check bucket policy metadata', () => { + const stack = new Stack(); + const sourceBucket = buildS3Bucket(stack, { + deployBucket: true + }); + CloudFrontDistributionForS3(stack, sourceBucket); + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { + Metadata: { + cfn_nag: { + rules_to_suppress: [ + { + id: "F16", + reason: "Public website bucket policy requires a wildcard principal" + } + ] + } + } + }, ResourcePart.CompleteDefinition); +}); + +test('check bucket metadata', () => { + const stack = new Stack(); + const sourceBucket = buildS3Bucket(stack, { + deployBucket: true + }); + CloudFrontDistributionForS3(stack, sourceBucket); + expect(stack).toHaveResource('AWS::S3::Bucket', { + Metadata: { + cfn_nag: { + rules_to_suppress: [ + { + id: "W35", + reason: "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + }, + { + id: "W51", + reason: "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + } + ] + } + } + }, ResourcePart.CompleteDefinition); +}); + +test('test cloudfront check bucket policy', () => { + const stack = new Stack(); + const sourceBucket = buildS3Bucket(stack, { + deployBucket: true + }); + CloudFrontDistributionForS3(stack, sourceBucket); + + expect(stack).toHaveResourceLike("AWS::S3::BucketPolicy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + Effect: "Allow", + Principal: { + AWS: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":iam::cloudfront:user/CloudFront Origin Access Identity ", + { + Ref: "CloudFrontOriginAccessIdentity" + } + ] + ] + } + }, + Resource: [ + { + "Fn::GetAtt": [ + "S3Bucket07682993", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "S3Bucket07682993", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + Action: "s3:GetObject", + Effect: "Allow", + Principal: { + CanonicalUser: { + "Fn::GetAtt": [ + "CloudFrontOriginAccessIdentity", + "S3CanonicalUserId" + ] + } + }, + Resource: { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "S3Bucket07682993", + "Arn" + ] + }, + "/*" + ] + ] + } + } + ], + Version: "2012-10-17" + } + }); +}); + +test('test cloudfront override properties', () => { + const stack = new Stack(); + const sourceBucket = buildS3Bucket(stack, { + deployBucket: true + }); + // Create CloudFront Origin Access Identity User + const cfnOrigAccessId = new cloudfront.CfnCloudFrontOriginAccessIdentity(stack, 'CloudFrontOriginAccessIdentity1', { + cloudFrontOriginAccessIdentityConfig: { + comment: 'Access S3 bucket content only through CloudFront' + } + }); + + const oaiImported = cloudfront.OriginAccessIdentity.fromOriginAccessIdentityName( + stack, + 'OAIImported1', + cfnOrigAccessId.ref + ); + + const props: cloudfront.CloudFrontWebDistributionProps = { + originConfigs: [ { + s3OriginSource: { + s3BucketSource: sourceBucket, + originAccessIdentity: oaiImported + }, + behaviors: [ { + isDefaultBehavior: true, + allowedMethods: cloudfront.CloudFrontAllowedMethods.ALL, + cachedMethods: cloudfront.CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS + } ] + } ] + }; + + CloudFrontDistributionForS3(stack, sourceBucket, props); + + expect(stack).toHaveResourceLike("AWS::CloudFront::Distribution", { + DistributionConfig: { + DefaultCacheBehavior: { + AllowedMethods: [ + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT" + ], + CachedMethods: [ + "GET", + "HEAD", + "OPTIONS" + ], + Compress: true, + ForwardedValues: { + Cookies: { + Forward: "none" + }, + QueryString: false + }, + TargetOriginId: "origin1", + ViewerProtocolPolicy: "redirect-to-https" + }, + DefaultRootObject: "index.html", + Enabled: true, + HttpVersion: "http2", + IPV6Enabled: true, + Logging: { + Bucket: { + "Fn::GetAtt": [ + "CloudfrontLoggingBucket3C3EFAA7", + "RegionalDomainName" + ] + }, + IncludeCookies: false + }, + Origins: [ + { + DomainName: { + "Fn::GetAtt": [ + "S3Bucket07682993", + "RegionalDomainName" + ] + }, + Id: "origin1", + S3OriginConfig: { + OriginAccessIdentity: { + "Fn::Join": [ + "", + [ + "origin-access-identity/cloudfront/", + { + Ref: "CloudFrontOriginAccessIdentity1" + } + ] + ] + } + } + } + ], + PriceClass: "PriceClass_100", + ViewerCertificate: { + CloudFrontDefaultCertificate: true + } + } + }); + }); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/cloudwatch-log-group.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/cloudwatch-log-group.test.ts new file mode 100644 index 000000000..de6bf6a97 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/cloudwatch-log-group.test.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as defaults from '../index'; +import '@aws-cdk/assert/jest'; +import * as logs from '@aws-cdk/aws-logs'; + +test('cw log group with default params', () => { + const stack = new Stack(); + new logs.LogGroup(stack, 'test-cw-logs-default', defaults.DefaultLogGroupProps()); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('cw log group with log group name', () => { + const stack = new Stack(); + new logs.LogGroup(stack, 'test-cw-logs-default', defaults.DefaultLogGroupProps('lambda-log-group')); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/congnito-helper.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/congnito-helper.test.ts new file mode 100644 index 000000000..d6d9e45b9 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/congnito-helper.test.ts @@ -0,0 +1,198 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as cognito from '@aws-cdk/aws-cognito'; +import * as defaults from '../index'; +import '@aws-cdk/assert/jest'; + +test('snapshot test buildUserPool default params', () => { + const stack = new Stack(); + defaults.buildUserPool(stack); + + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('snapshot test buildUserPoolClient default params', () => { + const stack = new Stack(); + const userpool = defaults.buildUserPool(stack); + defaults.buildUserPoolClient(stack, userpool); + + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('Test override for buildUserPool', () => { + const stack = new Stack(); + + const userpoolProps: cognito.UserPoolProps = { + userPoolName: 'test', + signInType: cognito.SignInType.EMAIL_OR_PHONE + }; + + defaults.buildUserPool(stack, userpoolProps); + + expect(stack).toHaveResource('AWS::Cognito::UserPool', { + LambdaConfig: {}, + UsernameAttributes: [ + "email", + "phone_number" + ], + UserPoolAddOns: { + AdvancedSecurityMode: "ENFORCED" + }, + UserPoolName: "test" + }); +}); + +test('Test override for buildUserPoolClient', () => { + const stack = new Stack(); + + const userpool = defaults.buildUserPool(stack); + + const userpoolclientProps: cognito.UserPoolClientProps = { + userPoolClientName: 'test', + userPool: userpool + }; + + defaults.buildUserPoolClient(stack, userpool, userpoolclientProps); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { + UserPoolId: { + Ref: "CognitoUserPool53E37E69" + }, + ClientName: "test" + }); +}); + +test('Test override for buildIdentityPool', () => { + const stack = new Stack(); + + const userpool = defaults.buildUserPool(stack); + const userpoolclient = defaults.buildUserPoolClient(stack, userpool, { + userPoolClientName: 'test', + userPool: userpool + }); + defaults.buildIdentityPool(stack, userpool, userpoolclient, { + allowUnauthenticatedIdentities: true + }); + + expect(stack).toHaveResource('AWS::Cognito::IdentityPool', { + AllowUnauthenticatedIdentities: true, + CognitoIdentityProviders: [ + { + ClientId: { + Ref: "CognitoUserPoolClient5AB59AE4" + }, + ProviderName: { + "Fn::GetAtt": [ + "CognitoUserPool53E37E69", + "ProviderName" + ] + }, + ServerSideTokenCheck: true + } + ] + }); +}); + +test('Test setupCognitoForElasticSearch', () => { + const stack = new Stack(); + + const userpool = defaults.buildUserPool(stack); + const userpoolclient = defaults.buildUserPoolClient(stack, userpool, { + userPoolClientName: 'test', + userPool: userpool + }); + const identitypool = defaults.buildIdentityPool(stack, userpool, userpoolclient); + + defaults.setupCognitoForElasticSearch(stack, 'test-domain', { + userpool, + userpoolclient, + identitypool + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolDomain', { + Domain: "test-domain" + }); + + expect(stack).toHaveResource('AWS::Cognito::IdentityPoolRoleAttachment', { + IdentityPoolId: { + Ref: "CognitoIdentityPool" + }, + Roles: { + authenticated: { + "Fn::GetAtt": [ + "CognitoAuthorizedRole14E74FE0", + "Arn" + ] + } + } + }); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: "sts:AssumeRoleWithWebIdentity", + Condition: { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + Ref: "CognitoIdentityPool" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "authenticated" + } + }, + Effect: "Allow", + Principal: { + Federated: "cognito-identity.amazonaws.com" + } + } + ], + Version: "2012-10-17" + }, + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: "es:ESHttp*", + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":domain/test-domain/*" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + PolicyName: "CognitoAccessPolicy" + } + ] + }); + +}); diff --git a/source/patterns/@aws-solutions-konstruk/core/test/dynamo-table.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/dynamo-table.test.ts new file mode 100644 index 000000000..8d2f9682e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/dynamo-table.test.ts @@ -0,0 +1,158 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as defaults from '../index'; +import { overrideProps } from '../lib/utils'; +import '@aws-cdk/assert/jest'; + +test('snapshot test TableProps default params', () => { + const stack = new Stack(); + new dynamodb.Table(stack, 'test-dynamo-defaults', defaults.DefaultTableProps); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('snapshot test TableWithStream default params', () => { + const stack = new Stack(); + new dynamodb.Table(stack, 'test-dynamo-stream-defaults', defaults.DefaultTableWithStreamProps); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('test TableProps change billing mode', () => { + const stack = new Stack(); + + const defaultProps: dynamodb.TableProps = defaults.DefaultTableProps; + + const inProps: dynamodb.TableProps = { + billingMode: dynamodb.BillingMode.PROVISIONED, + readCapacity: 3, + writeCapacity: 3, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + } + }; + + const outProps = overrideProps(defaultProps, inProps); + new dynamodb.Table(stack, 'test-dynamo-override', outProps); + + expect(stack).toHaveResource("AWS::DynamoDB::Table", { + KeySchema: [ + { + AttributeName: "id", + KeyType: "HASH" + } + ], + AttributeDefinitions: [ + { + AttributeName: "id", + AttributeType: "S" + } + ], + ProvisionedThroughput: { + ReadCapacityUnits: 3, + WriteCapacityUnits: 3 + }, + SSESpecification: { + SSEEnabled: true + } + }); +}); + +test('test TableProps override add sort key', () => { + const stack = new Stack(); + + const defaultProps: dynamodb.TableProps = defaults.DefaultTableProps; + + const inProps: dynamodb.TableProps = { + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + }, + sortKey: { + name: 'sort_key', + type: dynamodb.AttributeType.STRING + } + }; + + const outProps = overrideProps(defaultProps, inProps); + new dynamodb.Table(stack, 'test-dynamo-override', outProps); + + expect(stack).toHaveResource("AWS::DynamoDB::Table", { + KeySchema: [ + { + AttributeName: "id", + KeyType: "HASH" + }, + { + AttributeName: "sort_key", + KeyType: "RANGE" + } + ], + AttributeDefinitions: [ + { + AttributeName: "id", + AttributeType: "S" + }, + { + AttributeName: "sort_key", + AttributeType: "S" + } + ], + BillingMode: "PAY_PER_REQUEST", + SSESpecification: { + SSEEnabled: true + } + }); +}); + +test('test TableWithStreamProps override stream view type', () => { + const stack = new Stack(); + + const defaultProps: dynamodb.TableProps = defaults.DefaultTableWithStreamProps; + + const inProps: dynamodb.TableProps = { + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + }, + stream: dynamodb.StreamViewType.NEW_IMAGE + }; + + const outProps = overrideProps(defaultProps, inProps); + new dynamodb.Table(stack, 'test-dynamo-override', outProps); + + expect(stack).toHaveResource("AWS::DynamoDB::Table", { + KeySchema: [ + { + AttributeName: "id", + KeyType: "HASH" + } + ], + AttributeDefinitions: [ + { + AttributeName: "id", + AttributeType: "S" + } + ], + BillingMode: "PAY_PER_REQUEST", + SSESpecification: { + SSEEnabled: true + }, + StreamSpecification: { + StreamViewType: "NEW_IMAGE" + } + }); +}); diff --git a/source/patterns/@aws-solutions-konstruk/core/test/elasticsearch-helper.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/elasticsearch-helper.test.ts new file mode 100644 index 000000000..116ddad01 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/elasticsearch-helper.test.ts @@ -0,0 +1,315 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as elasticsearch from '@aws-cdk/aws-elasticsearch'; +import * as defaults from '../index'; +import '@aws-cdk/assert/jest'; + +function deployES(stack: Stack, domainName: string, cfnDomainProps?: elasticsearch.CfnDomainProps, + lambdaRoleARN?: string): elasticsearch.CfnDomain { + const userpool = defaults.buildUserPool(stack); + const userpoolclient = defaults.buildUserPoolClient(stack, userpool, { + userPoolClientName: 'test', + userPool: userpool + }); + const identitypool = defaults.buildIdentityPool(stack, userpool, userpoolclient); + + const cognitoAuthorizedRole = defaults.setupCognitoForElasticSearch(stack, 'test-domain', { + userpool, + userpoolclient, + identitypool + }); + + if (lambdaRoleARN) { + return defaults.buildElasticSearch(stack, domainName, { + userpool, + identitypool, + cognitoAuthorizedRoleARN: cognitoAuthorizedRole.roleArn, + serviceRoleARN: lambdaRoleARN + }, cfnDomainProps); + } else { + return defaults.buildElasticSearch(stack, domainName, { + userpool, + identitypool, + cognitoAuthorizedRoleARN: cognitoAuthorizedRole.roleArn + }, cfnDomainProps); + } +} + +test('snapshot test buildElasticSearch default params', () => { + const stack = new Stack(); + deployES(stack, 'test-domain'); + + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('Test override SnapshotOptions for buildElasticSearch', () => { + const stack = new Stack(); + deployES(stack, 'test-domain', { + snapshotOptions: { + automatedSnapshotStartHour: 5 + } + }); + + expect(stack).toHaveResource('AWS::Elasticsearch::Domain', { + AccessPolicies: { + Statement: [ + { + Action: "es:ESHttp*", + Effect: "Allow", + Principal: { + AWS: { + "Fn::GetAtt": [ + "CognitoAuthorizedRole14E74FE0", + "Arn" + ] + } + }, + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":domain/test-domain/*" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + CognitoOptions: { + Enabled: true, + IdentityPoolId: { + Ref: "CognitoIdentityPool" + }, + RoleArn: { + "Fn::GetAtt": [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn" + ] + }, + UserPoolId: { + Ref: "CognitoUserPool53E37E69" + } + }, + DomainName: "test-domain", + EBSOptions: { + EBSEnabled: true, + VolumeSize: 10 + }, + ElasticsearchClusterConfig: { + DedicatedMasterCount: 3, + DedicatedMasterEnabled: true, + InstanceCount: 3, + ZoneAwarenessConfig: { + AvailabilityZoneCount: 3 + }, + ZoneAwarenessEnabled: true + }, + ElasticsearchVersion: "6.3", + EncryptionAtRestOptions: { + Enabled: true + }, + NodeToNodeEncryptionOptions: { + Enabled: true + }, + SnapshotOptions: { + AutomatedSnapshotStartHour: 5 + } + }); +}); + +test('Test override ES version for buildElasticSearch', () => { + const stack = new Stack(); + deployES(stack, 'test-domain', { + elasticsearchVersion: '7.0' + }); + + expect(stack).toHaveResource('AWS::Elasticsearch::Domain', { + AccessPolicies: { + Statement: [ + { + Action: "es:ESHttp*", + Effect: "Allow", + Principal: { + AWS: { + "Fn::GetAtt": [ + "CognitoAuthorizedRole14E74FE0", + "Arn" + ] + } + }, + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":domain/test-domain/*" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + CognitoOptions: { + Enabled: true, + IdentityPoolId: { + Ref: "CognitoIdentityPool" + }, + RoleArn: { + "Fn::GetAtt": [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn" + ] + }, + UserPoolId: { + Ref: "CognitoUserPool53E37E69" + } + }, + DomainName: "test-domain", + EBSOptions: { + EBSEnabled: true, + VolumeSize: 10 + }, + ElasticsearchClusterConfig: { + DedicatedMasterCount: 3, + DedicatedMasterEnabled: true, + InstanceCount: 3, + ZoneAwarenessConfig: { + AvailabilityZoneCount: 3 + }, + ZoneAwarenessEnabled: true + }, + ElasticsearchVersion: "7.0", + EncryptionAtRestOptions: { + Enabled: true + }, + NodeToNodeEncryptionOptions: { + Enabled: true + }, + SnapshotOptions: { + AutomatedSnapshotStartHour: 1 + } + }); + +}); + +test('Test ES with lambdaRoleARN', () => { + const stack = new Stack(); + deployES(stack, 'test-domain', {}, 'arn:aws:us-east-1:mylambdaRoleARN'); + + expect(stack).toHaveResource('AWS::Elasticsearch::Domain', { + AccessPolicies: { + Statement: [ + { + Action: "es:ESHttp*", + Effect: "Allow", + Principal: { + AWS: [ + { + "Fn::GetAtt": [ + "CognitoAuthorizedRole14E74FE0", + "Arn" + ] + }, + "arn:aws:us-east-1:mylambdaRoleARN" + ] + }, + Resource: { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":domain/test-domain/*" + ] + ] + } + } + ], + Version: "2012-10-17" + }, + CognitoOptions: { + Enabled: true, + IdentityPoolId: { + Ref: "CognitoIdentityPool" + }, + RoleArn: { + "Fn::GetAtt": [ + "CognitoKibanaConfigureRole62CCE76A", + "Arn" + ] + }, + UserPoolId: { + Ref: "CognitoUserPool53E37E69" + } + }, + DomainName: "test-domain", + EBSOptions: { + EBSEnabled: true, + VolumeSize: 10 + }, + ElasticsearchClusterConfig: { + DedicatedMasterCount: 3, + DedicatedMasterEnabled: true, + InstanceCount: 3, + ZoneAwarenessConfig: { + AvailabilityZoneCount: 3 + }, + ZoneAwarenessEnabled: true + }, + ElasticsearchVersion: "6.3", + EncryptionAtRestOptions: { + Enabled: true + }, + NodeToNodeEncryptionOptions: { + Enabled: true + }, + SnapshotOptions: { + AutomatedSnapshotStartHour: 1 + } + }); + +}); + +test('Count ES CW Alarms', () => { + const stack = new Stack(); + deployES(stack, 'test-domain'); + const cwList = defaults.buildElasticSearchCWAlarms(stack); + + expect(cwList.length).toEqual(9); +}); diff --git a/source/patterns/@aws-solutions-konstruk/core/test/events-rule.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/events-rule.test.ts new file mode 100644 index 000000000..fc0f4af53 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/events-rule.test.ts @@ -0,0 +1,124 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as events from '@aws-cdk/aws-events'; +import * as defaults from '../index'; +import '@aws-cdk/assert/jest'; +import { Schedule } from '@aws-cdk/aws-events'; +import { Duration } from '@aws-cdk/core'; +import { overrideProps } from '../lib/utils'; + +test('snapshot test EventsRuleProps default params', () => { + const stack = new Stack(); + + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + const fn = defaults.deployLambdaFunction(stack, lambdaFunctionProps); + + const lambdaFunc: events.IRuleTarget = { + bind: () => ({ + id: '', + arn: fn.functionArn + }) + }; + + const defaultEventsRuleProps = defaults.DefaultEventsRuleProps([lambdaFunc]); + const eventsRuleProps = overrideProps(defaultEventsRuleProps, { + schedule: Schedule.rate(Duration.minutes(5)) + }); + + new events.Rule(stack, 'Events', eventsRuleProps); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('test EventsRuleProps override ruleName and description', () => { + const stack = new Stack(); + + const lambdaFunc: events.IRuleTarget = { + bind: () => ({ + id: '', + arn: 'ARN' + }) + }; + + const defaultEventsRuleProps = defaults.DefaultEventsRuleProps([lambdaFunc]); + const eventsRuleProps = overrideProps(defaultEventsRuleProps, { + ruleName: 'test', + description: 'hello world', + schedule: Schedule.rate(Duration.minutes(5)) + } as events.RuleProps); + + new events.Rule(stack, 'Events', eventsRuleProps); + + expect(stack).toHaveResource('AWS::Events::Rule', { + Description: "hello world", + Name: "test", + ScheduleExpression: "rate(5 minutes)", + State: "ENABLED", + Targets: [ + { + Arn: "ARN", + Id: "Target0" + } + ] + }); +}); + +test('test EventsRuleProps add more event targets', () => { + const stack = new Stack(); + + const lambdaFunc1: events.IRuleTarget = { + bind: () => ({ + id: '', + arn: 'ARN1' + }) + }; + + const defaultEventsRuleProps = defaults.DefaultEventsRuleProps([lambdaFunc1]); + + const lambdaFunc2: events.IRuleTarget = { + bind: () => ({ + id: '', + arn: 'ARN2' + }) + }; + + const eventsRuleProps = overrideProps(defaultEventsRuleProps, { + targets: [lambdaFunc2], + schedule: Schedule.rate(Duration.minutes(5)) + } as events.RuleProps, true); + + new events.Rule(stack, 'Events', eventsRuleProps); + + expect(stack).toHaveResource('AWS::Events::Rule', { + ScheduleExpression: "rate(5 minutes)", + State: "ENABLED", + Targets: [ + { + Arn: "ARN1", + Id: "Target0" + }, + { + Arn: "ARN2", + Id: "Target1" + } + ] + }); +}); diff --git a/source/patterns/@aws-solutions-konstruk/core/test/iot-rule.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/iot-rule.test.ts new file mode 100644 index 000000000..a697a0171 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/iot-rule.test.ts @@ -0,0 +1,118 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as iot from '@aws-cdk/aws-iot'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as defaults from '../index'; +import { overrideProps } from '../lib/utils'; +import '@aws-cdk/assert/jest'; + +test('snapshot test TopicRuleProps default params', () => { + const stack = new Stack(); + + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + const fn = new lambda.Function(stack, 'LambdaFunction', lambdaFunctionProps); + + const defaultIotTopicProps = defaults.DefaultCfnTopicRuleProps([{ + lambda: { + functionArn: fn.functionArn + } + }], "SELECT * FROM 'topic/#'"); + new iot.CfnTopicRule(stack, 'IotTopic', defaultIotTopicProps); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('test TopicRuleProps override sql and description', () => { + const stack = new Stack(); + + const action1: iot.CfnTopicRule.ActionProperty = { + lambda: { + functionArn: 'xyz' + } + }; + + const defaultProps: iot.CfnTopicRuleProps = defaults.DefaultCfnTopicRuleProps([action1]); + + const inProps: iot.CfnTopicRuleProps = { + topicRulePayload: { + ruleDisabled: true, + description: "Processing of vehicle messages", + sql: "SELECT * FROM 'connectedcar/#'", + actions: [] + } + }; + + const outProps = overrideProps(defaultProps, inProps, true); + + new iot.CfnTopicRule(stack, 'IotTopic', outProps); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Lambda: { + FunctionArn: "xyz" + } + } + ], + Description: "Processing of vehicle messages", + RuleDisabled: true, + Sql: "SELECT * FROM 'connectedcar/#'" + } + }); +}); + +test('test TopicRuleProps override actions', () => { + const stack = new Stack(); + + const defaultProps: iot.CfnTopicRuleProps = defaults.DefaultCfnTopicRuleProps([], ''); + + const action: iot.CfnTopicRule.ActionProperty = { + lambda: { + functionArn: 'abc' + } + }; + + const inProps: iot.CfnTopicRuleProps = { + topicRulePayload: { + ruleDisabled: true, + sql: '', + actions: [action] + } + }; + + const outProps = overrideProps(defaultProps, inProps); + + new iot.CfnTopicRule(stack, 'IotTopic', outProps); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Lambda: { + FunctionArn: "abc" + } + } + ], + RuleDisabled: true, + Sql: "" + } + }); +}); diff --git a/source/patterns/@aws-solutions-konstruk/core/test/kinesis-analytics-helper.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/kinesis-analytics-helper.test.ts new file mode 100644 index 000000000..9942a6439 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/kinesis-analytics-helper.test.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { Stack } from "@aws-cdk/core"; +import * as kinesisFirehose from "@aws-cdk/aws-kinesisfirehose"; +import * as defaults from '../'; +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +// -------------------------------------------------------------- +// Test default functionality +// -------------------------------------------------------------- +test('Test default functionality', () => { + // Setup the stack + const stack = new Stack(); + const firehose = new kinesisFirehose.CfnDeliveryStream(stack, 'KinesisFirehose'); + // Setup the Kinesis Analytics application + defaults.buildKinesisAnalyticsApp(stack, { + kinesisFirehose: firehose, + kinesisAnalyticsProps: { + inputs: [{ + inputSchema: { + recordColumns: [{ + name: 'ticker_symbol', + sqlType: 'VARCHAR(4)', + mapping: '$.ticker_symbol' + }, { + name: 'sector', + sqlType: 'VARCHAR(16)', + mapping: '$.sector' + }, { + name: 'change', + sqlType: 'REAL', + mapping: '$.change' + }, { + name: 'price', + sqlType: 'REAL', + mapping: '$.price' + }], + recordFormat: { + recordFormatType: 'JSON' + }, + recordEncoding: 'UTF-8' + }, + namePrefix: 'SOURCE_SQL_STREAM' + }] + } + }); + // Assertions + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/kinesis-analytics.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/kinesis-analytics.test.ts new file mode 100644 index 000000000..999a7f4c2 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/kinesis-analytics.test.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as kinesisanalytics from '@aws-cdk/aws-kinesisanalytics'; +import * as defaults from '../index'; +import { overrideProps } from '../lib/utils'; +import '@aws-cdk/assert/jest'; + +test('snapshot test kinesisanalytics default params', () => { + const stack = new Stack(); + new kinesisanalytics.CfnApplication(stack, 'KinesisAnalytics', defaults.DefaultCfnApplicationProps); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('test kinesisanalytics override inputProperty', () => { + const stack = new Stack(); + + const inputProperty: kinesisanalytics.CfnApplication.InputProperty = { + inputSchema: { + recordColumns: [{name: 'x', sqlType: 'y'}], + recordFormat: { recordFormatType: 'csv' } + }, + namePrefix: 'zzz' + }; + + const defaultProps: kinesisanalytics.CfnApplicationProps = defaults.DefaultCfnApplicationProps; + + const inProps: kinesisanalytics.CfnApplicationProps = { + inputs: [inputProperty] + }; + + const outProps = overrideProps(defaultProps, inProps); + + new kinesisanalytics.CfnApplication(stack, 'KinesisAnalytics', outProps); + + expect(stack).toHaveResource("AWS::KinesisAnalytics::Application", { + Inputs: [ + { + InputSchema: { + RecordColumns: [ + { + Name: "x", + SqlType: "y" + } + ], + RecordFormat: { + RecordFormatType: "csv" + } + }, + NamePrefix: "zzz" + } + ] + }); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/kinesis-firehose-s3-defaults.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/kinesis-firehose-s3-defaults.test.ts new file mode 100644 index 000000000..7c4f1ed59 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/kinesis-firehose-s3-defaults.test.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as kinesisfirehose from '@aws-cdk/aws-kinesisfirehose'; +import * as defaults from '../index'; +import { overrideProps } from '../lib/utils'; +import '@aws-cdk/assert/jest'; + +test('snapshot test kinesisfirehose default params', () => { + const stack = new Stack(); + new kinesisfirehose.CfnDeliveryStream(stack, 'KinesisFirehose', + defaults.DefaultCfnDeliveryStreamProps('bucket_arn', 'role_arn', 'log_group', 'log_stream')); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('test kinesisanalytics override buffer conditions', () => { + const stack = new Stack(); + + const inProps = { + extendedS3DestinationConfiguration: { + bufferingHints: { + intervalInSeconds: 600, + sizeInMBs: 10 + }, + } + }; + + const defaultProps = defaults.DefaultCfnDeliveryStreamProps('bucket_arn', 'role_arn', 'log_group', 'log_stream'); + + const outProps = overrideProps(defaultProps, inProps); + + new kinesisfirehose.CfnDeliveryStream(stack, 'KinesisFirehose', outProps); + + expect(stack).toHaveResource("AWS::KinesisFirehose::DeliveryStream", { + ExtendedS3DestinationConfiguration: { + BucketARN: "bucket_arn", + BufferingHints: { + IntervalInSeconds: 600, + SizeInMBs: 10 + }, + CloudWatchLoggingOptions: { + Enabled: true, + LogGroupName: "log_group", + LogStreamName: "log_stream" + }, + CompressionFormat: "GZIP", + RoleARN: "role_arn" + } + }); +}); diff --git a/source/patterns/@aws-solutions-konstruk/core/test/kinesis-streams-defaults.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/kinesis-streams-defaults.test.ts new file mode 100644 index 000000000..a3862a026 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/kinesis-streams-defaults.test.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as defaults from '../index'; +import { overrideProps } from '../lib/utils'; +import '@aws-cdk/assert/jest'; + +test('snapshot test kinesisstream default params', () => { + const stack = new Stack(); + new kinesis.Stream(stack, 'KinesisStream', defaults.DefaultStreamProps); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('test kinesisstream override RetentionPeriodHours', () => { + const stack = new Stack(); + + const defaultProps = defaults.DefaultStreamProps; + + const inProps: kinesis.StreamProps = { + retentionPeriodHours: 48 + }; + + const outProps = overrideProps(defaultProps, inProps); + + new kinesis.Stream(stack, 'KinesisStream', outProps); + + expect(stack).toHaveResource("AWS::Kinesis::Stream", { + RetentionPeriodHours: 48 + }); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/kinesis-streams-helper.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/kinesis-streams-helper.test.ts new file mode 100644 index 000000000..94bfe492d --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/kinesis-streams-helper.test.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { Stack } from "@aws-cdk/core"; +import * as defaults from '../'; +import { SynthUtils, ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +// -------------------------------------------------------------- +// Test minimal deployment with no properties +// -------------------------------------------------------------- +test('Test minimal deployment with no properties', () => { + // Stack + const stack = new Stack(); + // Helper declaration + defaults.buildKinesisStream(stack); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // Assertion 2 + expect(stack).toHaveResourceLike('AWS::Kinesis::Stream', { + Type: "AWS::Kinesis::Stream", + Properties: { + StreamEncryption: { + EncryptionType: "KMS" + } + } + }, ResourcePart.CompleteDefinition); +}); + +// -------------------------------------------------------------- +// Test deployment w/ custom properties +// -------------------------------------------------------------- +test('Test deployment w/ custom properties', () => { + // Stack + const stack = new Stack(); + // Helper setup + const encKey = defaults.buildEncryptionKey(stack); + // Helper declaration + defaults.buildKinesisStream(stack, { + encryptionKey: encKey, + kinesisStreamProps: { + streamName: 'myCustomKinesisStream' + } + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // Assertion 2 + expect(stack).toHaveResource('AWS::Kinesis::Stream', { + Name: 'myCustomKinesisStream' + }); + // Assertion 3 + expect(stack).toHaveResource('AWS::KMS::Key', { + EnableKeyRotation: true + }); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/kms-helper.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/kms-helper.test.ts new file mode 100644 index 000000000..c9e6c0d9e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/kms-helper.test.ts @@ -0,0 +1,60 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { Stack } from "@aws-cdk/core"; +import * as defaults from '../'; +import { SynthUtils, ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +// -------------------------------------------------------------- +// Test minimal deployment with no properties +// -------------------------------------------------------------- +test('Test minimal deployment with no properties', () => { + // Stack + const stack = new Stack(); + // Helper declaration + defaults.buildEncryptionKey(stack); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // Assertion 2 + expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Type: "AWS::KMS::Key", + Properties: { + EnableKeyRotation: true + } + }, ResourcePart.CompleteDefinition); +}); + +// -------------------------------------------------------------- +// Test deployment w/ custom properties +// -------------------------------------------------------------- +test('Test minimal deployment with no properties', () => { + // Stack + const stack = new Stack(); + // Helper declaration + defaults.buildEncryptionKey(stack, { + encryptionKeyProps: { + enableKeyRotation: false + } + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // Assertion 2 + expect(stack).toHaveResourceLike('AWS::KMS::Key', { + Type: "AWS::KMS::Key", + Properties: { + EnableKeyRotation: false + } + }, ResourcePart.CompleteDefinition); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/lambda-event-source.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/lambda-event-source.test.ts new file mode 100644 index 000000000..18efbdacf --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/lambda-event-source.test.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as defaults from '../index'; +import { DynamoEventSourceProps } from '@aws-cdk/aws-lambda-event-sources'; +import * as lambda from '@aws-cdk/aws-lambda'; +import '@aws-cdk/assert/jest'; + +test('test DynamoEventSourceProps', () => { + const props = defaults.DynamoEventSourceProps(); + + expect(props).toEqual({ + startingPosition: "TRIM_HORIZON" + }); +}); + +test('test DynamoEventSourceProps override', () => { + const myProps: DynamoEventSourceProps = { + startingPosition: lambda.StartingPosition.LATEST, + batchSize: 1 + }; + + const props = defaults.DynamoEventSourceProps(myProps); + + expect(props).toEqual({ + batchSize: 1, + startingPosition: "LATEST" + }); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/lambda-func.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/lambda-func.test.ts new file mode 100644 index 000000000..9736045d7 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/lambda-func.test.ts @@ -0,0 +1,232 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils, ResourcePart } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as defaults from '../index'; +import '@aws-cdk/assert/jest'; +import { Duration } from '@aws-cdk/core'; + +test('snapshot test LambdaFunction default params', () => { + const stack = new Stack(); + + const lambdaFunctionProps: lambda.FunctionProps = { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + }; + + defaults.deployLambdaFunction(stack, lambdaFunctionProps); + + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('test FunctionProps override code and runtime', () => { + const stack = new Stack(); + + const inProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda-test`), + runtime: lambda.Runtime.PYTHON_3_6, + handler: 'index.handler' + }; + + defaults.deployLambdaFunction(stack, inProps); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + Runtime: "python3.6" + }); +}); + +test('test FunctionProps override timeout', () => { + const stack = new Stack(); + + const inProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + timeout: Duration.seconds(5), + }; + + defaults.deployLambdaFunction(stack, inProps); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + Runtime: "nodejs12.x", + Timeout: 5 + }); +}); + +test('test FunctionProps for envrionment variable when runtime = NODEJS', () => { + const stack = new Stack(); + + const inProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }; + + defaults.deployLambdaFunction(stack, inProps); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + Runtime: "nodejs10.x", + Environment: { + Variables: { + AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1' + } + } + }); + +}); + +test('test FunctionProps for no envrionment variable when runtime = PYTHON', () => { + const stack = new Stack(); + + const inProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda-test`), + runtime: lambda.Runtime.PYTHON_3_6, + handler: 'index.handler' + }; + + defaults.deployLambdaFunction(stack, inProps); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Type: "AWS::Lambda::Function", + Properties: { + Code: { + S3Bucket: { + Ref: "AssetParametersb472c1cea6f4795d84eb1b97e37bfa1f79f1c744caebeb372f30dbf716299895S3Bucket0A3514D6" + }, + S3Key: { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + Ref: "AssetParametersb472c1cea6f4795d84eb1b97e37bfa1f79f1c744caebeb372f30dbf716299895S3VersionKey0DB6BEDE" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + Ref: "AssetParametersb472c1cea6f4795d84eb1b97e37bfa1f79f1c744caebeb372f30dbf716299895S3VersionKey0DB6BEDE" + } + ] + } + ] + } + ] + ] + } + }, + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + Runtime: "python3.6" + }, + DependsOn: [ + "LambdaFunctionServiceRole0C4CDE0B" + ] + }, ResourcePart.CompleteDefinition); + +}); + +test('test buildLambdaFunction with deploy = true', () => { + const stack = new Stack(); + + const inProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda-test`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }; + + defaults.buildLambdaFunction(stack, { + deployLambda: true, + lambdaFunctionProps: inProps + }); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + Runtime: "nodejs12.x" + }); +}); + +test('test buildLambdaFunction with deploy = false', () => { + const stack = new Stack(); + + const inProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda-test`), + runtime: lambda.Runtime.PYTHON_3_6, + handler: 'index.handler' + }; + + const fn = defaults.deployLambdaFunction(stack, inProps); + + defaults.buildLambdaFunction(stack, { + deployLambda: false, + existingLambdaObj: fn + }); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Handler: "index.handler", + Role: { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + Runtime: "python3.6" + }); +}); diff --git a/source/patterns/@aws-solutions-konstruk/core/test/lambda-test/index.js b/source/patterns/@aws-solutions-konstruk/core/test/lambda-test/index.js new file mode 100644 index 000000000..b12fff204 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/lambda-test/index.js @@ -0,0 +1,7 @@ +exports.handler = async function(event) { + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: `Hello, CDK! You've hit ${event.path}\n` + }; + }; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/lambda/index.js b/source/patterns/@aws-solutions-konstruk/core/test/lambda/index.js new file mode 100644 index 000000000..4b3640c1e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/lambda/index.js @@ -0,0 +1,10 @@ +console.log('Loading function'); + +exports.handler = async (event, context) => { + console.log('Received event:', JSON.stringify(event, null, 2)); +    return { +      statusCode: 200, +      headers: { 'Content-Type': 'text/plain' }, +      body: `Hello from Project Vesper! You've hit ${event.path}\n` +    }; +}; \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/s3-bucket-helper.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/s3-bucket-helper.test.ts new file mode 100644 index 000000000..dab7a659a --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/s3-bucket-helper.test.ts @@ -0,0 +1,59 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as defaults from '../index'; + +test('s3 bucket with default params', () => { + const stack = new Stack(); + defaults.buildS3Bucket(stack, { + deployBucket: true + }); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('s3 bucket with default params and bucket names', () => { + const stack = new Stack(); + const s3BucketProps: s3.BucketProps = { + bucketName: 'my-bucket' + }; + defaults.buildS3Bucket(stack, { + deployBucket: true, + bucketProps: s3BucketProps + }); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('s3 bucket with existingBucketObj', () => { + const stack = new Stack(); + + defaults.buildS3Bucket(stack, { + deployBucket: false, + existingBucketObj: new s3.Bucket(stack, 'my-bucket', {}) + }); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check exception for Missing existingBucketObj from props for deploy = false', () => { + const stack = new Stack(); + + try { + defaults.buildS3Bucket(stack, { + deployBucket: false + }); + } catch (e) { + expect(e).toBeInstanceOf(Error); + } +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/s3-bucket.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/s3-bucket.test.ts new file mode 100644 index 000000000..0fec10b05 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/s3-bucket.test.ts @@ -0,0 +1,184 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { SynthUtils } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as defaults from '../index'; +import { overrideProps } from '../lib/utils'; +import '@aws-cdk/assert/jest'; + +test('s3 bucket with default params', () => { + const stack = new Stack(); + new s3.Bucket(stack, 'test-s3-defaults', defaults.DefaultS3Props()); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('test s3Bucket override versioningConfiguration', () => { + const stack = new Stack(); + const defaultProps: s3.CfnBucketProps = defaults.DefaultS3Props(); + + const inProps: s3.CfnBucketProps = { + versioningConfiguration: { + status: 'Disabled' + }, + }; + + const outProps = overrideProps(defaultProps, inProps); + new s3.CfnBucket(stack, 'test-s3-verioning', outProps); + + expect(stack).toHaveResource("AWS::S3::Bucket", { + VersioningConfiguration: { + Status: 'Disabled' + } + }); +}); + +test('test s3Bucket override bucketEncryption', () => { + const stack = new Stack(); + const defaultProps: s3.CfnBucketProps = defaults.DefaultS3Props(); + + const inProps: s3.CfnBucketProps = { + bucketEncryption: { + serverSideEncryptionConfiguration : [{ + serverSideEncryptionByDefault: { + kmsMasterKeyId: 'mykeyid', + sseAlgorithm: 'aws:kms' + } + }] + }, + }; + + const outProps = overrideProps(defaultProps, inProps); + new s3.CfnBucket(stack, 'test-s3-encryption', outProps); + + expect(stack).toHaveResource("AWS::S3::Bucket", { + BucketEncryption: { + ServerSideEncryptionConfiguration : [{ + ServerSideEncryptionByDefault: { + KMSMasterKeyID: 'mykeyid', + SSEAlgorithm: 'aws:kms' + } + }] + }, + }); +}); + +test('test s3Bucket override publicAccessBlockConfiguration', () => { + const stack = new Stack(); + const defaultProps: s3.CfnBucketProps = defaults.DefaultS3Props(); + + const inProps: s3.CfnBucketProps = { + publicAccessBlockConfiguration: { + blockPublicAcls: false, + blockPublicPolicy: true, + ignorePublicAcls: false, + restrictPublicBuckets: true + }, + }; + + const outProps = overrideProps(defaultProps, inProps); + new s3.CfnBucket(stack, 'test-s3-publicAccessBlock', outProps); + + expect(stack).toHaveResource("AWS::S3::Bucket", { + PublicAccessBlockConfiguration: { + BlockPublicAcls: false, + BlockPublicPolicy: true, + IgnorePublicAcls: false, + RestrictPublicBuckets: true + }, + }); +}); + +test('test s3Bucket add lifecycleConfiguration', () => { + const stack = new Stack(); + const defaultProps: s3.CfnBucketProps = defaults.DefaultS3Props(); + + const inProps: s3.CfnBucketProps = { + lifecycleConfiguration: { + rules: [ + { + status: 'Enabled', + expirationInDays: 365, + } + ] + } + }; + + const outProps = overrideProps(defaultProps, inProps); + new s3.CfnBucket(stack, 'test-s3-lifecycle', outProps); + + expect(stack).toHaveResource("AWS::S3::Bucket", { + LifecycleConfiguration: { + Rules: [ + { + Status: 'Enabled', + ExpirationInDays: 365, + } + ] + } + }); +}); + +test('test s3Bucket add objectLock', () => { + const stack = new Stack(); + const defaultProps: s3.CfnBucketProps = defaults.DefaultS3Props(); + + const inProps: s3.CfnBucketProps = { + objectLockConfiguration: { + objectLockEnabled: 'Enabled', + rule: { + defaultRetention: { + days: 365 + } + } + }, + objectLockEnabled: true, + }; + + const outProps = overrideProps(defaultProps, inProps); + new s3.CfnBucket(stack, 'test-s3-objlock', outProps); + + expect(stack).toHaveResource("AWS::S3::Bucket", { + ObjectLockConfiguration: { + ObjectLockEnabled: 'Enabled', + Rule: { + DefaultRetention: { + Days: 365 + } + } + }, + ObjectLockEnabled: true + }); +}); + +test('test s3Bucket override serverAccessLogsBucket', () => { + const stack = new Stack(); + + const myLoggingBucket: s3.Bucket = new s3.Bucket(stack, 'MyS3LoggingBucket', defaults.DefaultS3Props()); + + const myS3Props: s3.BucketProps = defaults.DefaultS3Props(myLoggingBucket); + + defaults.buildS3Bucket(stack, { + deployBucket: true, + bucketProps: myS3Props + }); + + expect(stack).toHaveResource("AWS::S3::Bucket", { + LoggingConfiguration: { + DestinationBucketName: { + Ref: "MyS3LoggingBucket119BE896" + } + } + }); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/sns-helper.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/sns-helper.test.ts new file mode 100644 index 000000000..32236f5e6 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/sns-helper.test.ts @@ -0,0 +1,77 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { Stack } from "@aws-cdk/core"; +import * as defaults from '../'; +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +// -------------------------------------------------------------- +// Test deployment with no properties +// -------------------------------------------------------------- +test('Test deployment with no properties', () => { + // Stack + const stack = new Stack(); + // Helper declaration + defaults.buildTopic(stack); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test deployment w/ custom properties +// -------------------------------------------------------------- +test('Test deployment w/ custom properties', () => { + // Stack + const stack = new Stack(); + // Helper declaration + defaults.buildTopic(stack, { + topicProps: { + topicName: "custom-topic" + }, + enableEncryption: true + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // Assertion 2 + expect(stack).toHaveResource("AWS::SNS::Topic", { + TopicName: "custom-topic" + }); +}); + +// -------------------------------------------------------------- +// Test deployment w/ imported encryption key +// -------------------------------------------------------------- +test('Test deployment w/ imported encryption key', () => { + // Stack + const stack = new Stack(); + // Helper declaration + defaults.buildTopic(stack, { + topicProps: { + topicName: "custom-topic" + }, + enableEncryption: true, + encryptionKey: defaults.buildEncryptionKey(stack) + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); + // Assertion 2 + expect(stack).toHaveResource("AWS::SNS::Topic", { + TopicName: "custom-topic" + }); + // Assertion 3 + expect(stack).toHaveResource("AWS::KMS::Key", { + EnableKeyRotation: true + }); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/core/test/sqs-helper.test.ts b/source/patterns/@aws-solutions-konstruk/core/test/sqs-helper.test.ts new file mode 100644 index 000000000..eee43c39e --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/core/test/sqs-helper.test.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { Stack } from "@aws-cdk/core"; +import * as defaults from '../'; +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +// -------------------------------------------------------------- +// Test minimal deployment with no properties +// -------------------------------------------------------------- +test('Test minimal deployment with no properties', () => { + // Stack + const stack = new Stack(); + // Helper declaration + defaults.buildQueue(stack, 'primary-queue'); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test deployment w/ custom properties +// -------------------------------------------------------------- +test('Test deployment w/ custom properties', () => { + // Stack + const stack = new Stack(); + // Helper setup + const encKey = defaults.buildEncryptionKey(stack); + // Helper declaration + defaults.buildQueue(stack, 'primary-queue', { + encryptionKey: encKey, + queueProps: { + description: "custom-queue-props" + } + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Test dead letter queue deployment/configuration +// -------------------------------------------------------------- +test('Test dead letter queue deployment/configuration', () => { + // Stack + const stack = new Stack(); + // Helper setup + const encKey = defaults.buildEncryptionKey(stack); + const dlq = defaults.buildQueue(stack, 'dead-letter-queue'); + const dlqi = defaults.buildDeadLetterQueue({ + deadLetterQueue: dlq, + maxReceiveCount: 3 + }); + // Helper declaration + defaults.buildQueue(stack, 'primary-queue', { + encryptionKey: encKey, + queueProps: { + description: "not-the-dead-letter-queue-props" + }, + deadLetterQueue: dlqi + }); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-konstruk/eslintrc.yml b/source/patterns/@aws-solutions-konstruk/eslintrc.yml new file mode 100644 index 000000000..6806f7be5 --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/eslintrc.yml @@ -0,0 +1,49 @@ +--- +env: + jest: true + node: true + +plugins: + - '@typescript-eslint' + - import + - license-header + +parser: '@typescript-eslint/parser' +parserOptions: + ecmaVersion: 2018 + sourceType: module + project: ./tsconfig.json + +extends: + - plugin:import/typescript + +settings: + import/parsers: + '@typescript-eslint/parser': ['.ts', '.tsx'] + import/resolver: + node: {} + typescript: + directory: ./tsconfig.json + +rules: + # Require use of the `import { foo } from 'bar';` form instead of `import foo = require('bar');` + '@typescript-eslint/no-require-imports': + - error + + # Require all imported dependencies are actually declared in package.json + 'import/no-extraneous-dependencies': + - error + - devDependencies: # Only allow importing devDependencies from: + - '**/test/**' # --> Unit tests + - '**/utils.ts' # --> uses deepmerge + optionalDependencies: false # Disallow importing optional dependencies (those shouldn't be in use in the project) + peerDependencies: false # Disallow importing peer dependencies (that aren't also direct dependencies) + + # Require all imported libraries actually resolve (!!required for import/no-extraneous-dependencies to work!!) + 'import/no-unresolved': + - error + + #Check for license header + 'license-header/header': + - error + - ../license-header.js diff --git a/source/patterns/@aws-solutions-konstruk/license-header.js b/source/patterns/@aws-solutions-konstruk/license-header.js new file mode 100644 index 000000000..e3d94bb9f --- /dev/null +++ b/source/patterns/@aws-solutions-konstruk/license-header.js @@ -0,0 +1,12 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ \ No newline at end of file diff --git a/source/use_cases/aws-s3-static-website/.eslintignore b/source/use_cases/aws-s3-static-website/.eslintignore new file mode 100644 index 000000000..8773c034a --- /dev/null +++ b/source/use_cases/aws-s3-static-website/.eslintignore @@ -0,0 +1,6 @@ +lib/*.js +test/*.js +bin/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/use_cases/aws-s3-static-website/.gitignore b/source/use_cases/aws-s3-static-website/.gitignore new file mode 100644 index 000000000..96e33d0f7 --- /dev/null +++ b/source/use_cases/aws-s3-static-website/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +bin/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/use_cases/aws-s3-static-website/.npmignore b/source/use_cases/aws-s3-static-website/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/use_cases/aws-s3-static-website/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/use_cases/aws-s3-static-website/README.md b/source/use_cases/aws-s3-static-website/README.md new file mode 100644 index 000000000..258a17b28 --- /dev/null +++ b/source/use_cases/aws-s3-static-website/README.md @@ -0,0 +1,23 @@ +# AWS S3 Static Website Use Case + +This use case implements a static website that delivers HTML, JavaScript, images, video and other files to your website visitors and contain no application code. + +## Architecture +The application architecture uses an Amazon CloudFront distribution, Amazon S3 and AWS lambda based custom resource to copy the static website content for Wild Rydes demo website. +![Architecture Diagram](architecture.png) + +## Deployment steps +Below are the steps to deploy the use case: + +``` +npm run build + +cdk deploy + +``` + +## Deployment Verification +After the stack is deployed successfully, go to the Outputs tab in AWS Cloudformation console, it should show the 'websiteURL', click on the link and enjoy the Wile Rydes Unicorn website. + +*** +© Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/use_cases/aws-s3-static-website/architecture.png b/source/use_cases/aws-s3-static-website/architecture.png new file mode 100644 index 000000000..967bc554f Binary files /dev/null and b/source/use_cases/aws-s3-static-website/architecture.png differ diff --git a/source/use_cases/aws-s3-static-website/bin/s3-static-site-app.ts b/source/use_cases/aws-s3-static-website/bin/s3-static-site-app.ts new file mode 100644 index 000000000..4bb90921c --- /dev/null +++ b/source/use_cases/aws-s3-static-website/bin/s3-static-site-app.ts @@ -0,0 +1,22 @@ +#!/usr/bin/env node + +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import 'source-map-support/register'; +import * as cdk from '@aws-cdk/core'; +import { S3StaticWebsiteStack } from '../lib/s3-static-site-stack'; + +const app = new cdk.App(); +const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); +stack.templateOptions.description = 'Creates a static website using S3 for the Wild Rydes serverless web application workshop'; \ No newline at end of file diff --git a/source/use_cases/aws-s3-static-website/cdk.json b/source/use_cases/aws-s3-static-website/cdk.json new file mode 100644 index 000000000..ed6e4df88 --- /dev/null +++ b/source/use_cases/aws-s3-static-website/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node bin/s3-static-site-app.ts" +} \ No newline at end of file diff --git a/source/use_cases/aws-s3-static-website/lib/lambda/copy_s3_objects.py b/source/use_cases/aws-s3-static-website/lib/lambda/copy_s3_objects.py new file mode 100644 index 000000000..cdfd4406c --- /dev/null +++ b/source/use_cases/aws-s3-static-website/lib/lambda/copy_s3_objects.py @@ -0,0 +1,56 @@ +import os +import json +import boto3 +from botocore.exceptions import ClientError +client = boto3.client('s3') + +import logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +def on_event(event, context): + logger.info("Received event: %s" % json.dumps(event)) + request_type = event['RequestType'] + if request_type == 'Create': return on_create(event) + if request_type == 'Update': return on_create(event) + if request_type == 'Delete': return on_delete(event) + raise Exception("Invalid request type: %s" % request_type) + +def on_create(event): + source_bucket = event['ResourceProperties']['SourceBucket'] + source_prefix = event['ResourceProperties'].get('SourcePrefix') or '' + bucket = event['ResourceProperties']['Bucket'] + prefix = event['ResourceProperties'].get('Prefix') or '' + try: + copy_objects(source_bucket, source_prefix, bucket, prefix) + except ClientError as e: + logger.error('Error: %s', e) + raise e + return + +def on_delete(event): + bucket = event['ResourceProperties']['Bucket'] + prefix = event['ResourceProperties'].get('Prefix') or '' + try: + delete_objects(bucket, prefix) + except ClientError as e: + logger.error('Error: %s', e) + raise e + return + +def copy_objects(source_bucket, source_prefix, bucket, prefix): + paginator = client.get_paginator('list_objects_v2') + page_iterator = paginator.paginate(Bucket=source_bucket, Prefix=source_prefix) + for key in {x['Key'] for page in page_iterator for x in page['Contents']}: + dest_key = os.path.join(prefix, os.path.relpath(key, source_prefix)) + if not key.endswith('/'): + logger.info("copy %s to %s".format(key, dest_key)) + client.copy_object(CopySource={'Bucket': source_bucket, 'Key': key}, Bucket=bucket, Key = dest_key) + return + +def delete_objects(bucket, prefix): + paginator = client.get_paginator('list_objects_v2') + page_iterator = paginator.paginate(Bucket=bucket, Prefix=prefix) + objects = [{'Key': x['Key']} for page in page_iterator for x in page['Contents']] + client.delete_objects(Bucket=bucket, Delete={'Objects': objects}) + return \ No newline at end of file diff --git a/source/use_cases/aws-s3-static-website/lib/s3-static-site-stack.ts b/source/use_cases/aws-s3-static-website/lib/s3-static-site-stack.ts new file mode 100644 index 000000000..a3df7e9fc --- /dev/null +++ b/source/use_cases/aws-s3-static-website/lib/s3-static-site-stack.ts @@ -0,0 +1,77 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { Construct, Stack, StackProps, Duration, CfnOutput } from '@aws-cdk/core'; +import { CloudFrontToS3 } from '@aws-solutions-konstruk/aws-cloudfront-s3'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { Provider } from '@aws-cdk/custom-resources'; +import { CustomResource } from '@aws-cdk/aws-cloudformation'; +import { PolicyStatement } from '@aws-cdk/aws-iam'; + +export class S3StaticWebsiteStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const sourceBucket: string = 'wildrydes-us-east-1'; + const sourcePrefix: string = 'WebApplication/1_StaticWebHosting/website/'; + + const konstruk = new CloudFrontToS3(this, 'CloudFrontToS3', { + deployBucket: true + }); + const targetBucket: string = konstruk.bucket().bucketName; + + const lambdaFunc = new lambda.Function(this, 'copyObjHandler', { + runtime: lambda.Runtime.PYTHON_3_8, + handler: 'copy_s3_objects.on_event', + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + timeout: Duration.minutes(5), + initialPolicy: [ + new PolicyStatement({ + actions: ["s3:GetObject", + "s3:ListBucket"], + resources: [`arn:aws:s3:::${sourceBucket}`, + `arn:aws:s3:::${sourceBucket}/${sourcePrefix}*`] + }), + new PolicyStatement({ + actions: ["s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl", + "s3:DeleteObject", + "s3:DeleteObjectVersion", + "s3:CopyObject"], + resources: [`arn:aws:s3:::${targetBucket}`, + `arn:aws:s3:::${targetBucket}/*`] + }), + ] + }); + + const customResourceProvider = new Provider(this, 'CustomResourceProvider', { + onEventHandler: lambdaFunc + }); + + new CustomResource(this, 'CustomResource', { + provider: customResourceProvider, + properties: { + SourceBucket: sourceBucket, + SourcePrefix: sourcePrefix, + Bucket: targetBucket + } + }); + + new CfnOutput(this, 'websiteURL', { + value: 'https://' + konstruk.cloudFrontWebDistribution().domainName + }); + } +} \ No newline at end of file diff --git a/source/use_cases/aws-s3-static-website/package.json b/source/use_cases/aws-s3-static-website/package.json new file mode 100644 index 000000000..be6e3dd86 --- /dev/null +++ b/source/use_cases/aws-s3-static-website/package.json @@ -0,0 +1,52 @@ +{ + "name": "@aws-solutions-konstruk/aws-s3-static-website", + "version": "0.8.0", + "description": "Use case pattern for deploying a S3 static website.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/use_cases/aws-s3-static-website" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "build+lint+test": "npm run build && npm run lint && npm test && npm run integ-assert" + }, + "dependencies": { + "@aws-solutions-konstruk/aws-cloudfront-s3": "~0.8.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-cloudfront": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-cdk/custom-resources": "~1.25.0", + "@aws-cdk/aws-cloudformation": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "source-map-support": "^0.5.16" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + } +} diff --git a/source/use_cases/aws-s3-static-website/test/__snapshots__/s3-static-site-stack.test.js.snap b/source/use_cases/aws-s3-static-website/test/__snapshots__/s3-static-site-stack.test.js.snap new file mode 100644 index 000000000..2ab898e31 --- /dev/null +++ b/source/use_cases/aws-s3-static-website/test/__snapshots__/s3-static-site-stack.test.js.snap @@ -0,0 +1,626 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`default stack 1`] = ` +Object { + "Outputs": Object { + "websiteURL": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E", + "DomainName", + ], + }, + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debArtifactHashA2BE913F": Object { + "Description": "Artifact hash for asset \\"2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503deb\\"", + "Type": "String", + }, + "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3BucketC3836BD9": Object { + "Description": "S3 bucket for asset \\"2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503deb\\"", + "Type": "String", + }, + "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3VersionKeyEDA18BF9": Object { + "Description": "S3 key for asset version \\"2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503deb\\"", + "Type": "String", + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9ArtifactHash72EE40C1": Object { + "Description": "Artifact hash for asset \\"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\\"", + "Type": "String", + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3Bucket6B4B2C9B": Object { + "Description": "S3 bucket for asset \\"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\\"", + "Type": "String", + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19": Object { + "Description": "S3 key for asset version \\"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\\"", + "Type": "String", + }, + }, + "Resources": Object { + "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E": Object { + "Properties": Object { + "DistributionConfig": Object { + "DefaultCacheBehavior": Object { + "AllowedMethods": Array [ + "GET", + "HEAD", + ], + "CachedMethods": Array [ + "GET", + "HEAD", + ], + "Compress": true, + "ForwardedValues": Object { + "Cookies": Object { + "Forward": "none", + }, + "QueryString": false, + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https", + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": Object { + "Bucket": Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + "RegionalDomainName", + ], + }, + "IncludeCookies": false, + }, + "Origins": Array [ + Object { + "DomainName": Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3S3Bucket9CE6AB04", + "RegionalDomainName", + ], + }, + "Id": "origin1", + "S3OriginConfig": Object { + "OriginAccessIdentity": Object { + "Fn::Join": Array [ + "", + Array [ + "origin-access-identity/cloudfront/", + Object { + "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + }, + ], + ], + }, + }, + }, + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": Object { + "CloudFrontDefaultCertificate": true, + }, + }, + }, + "Type": "AWS::CloudFront::Distribution", + }, + "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91": Object { + "Properties": Object { + "CloudFrontOriginAccessIdentityConfig": Object { + "Comment": "Access S3 bucket content only through CloudFront", + }, + }, + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + }, + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + Object { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "CloudFrontToS3S3Bucket9CE6AB04": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "CloudFrontToS3S3LoggingBucketEF5CD8B2", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "CloudFrontToS3S3BucketPolicy2495300D": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "F16", + "reason": "Public website bucket policy requires a wildcard principal", + }, + ], + }, + }, + "Properties": Object { + "Bucket": Object { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::cloudfront:user/CloudFront Origin Access Identity ", + Object { + "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + }, + ], + ], + }, + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3S3Bucket9CE6AB04", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3S3Bucket9CE6AB04", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": "s3:GetObject", + "Effect": "Allow", + "Principal": Object { + "CanonicalUser": Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + "S3CanonicalUserId", + ], + }, + }, + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3S3Bucket9CE6AB04", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "CloudFrontToS3S3LoggingBucketEF5CD8B2": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "CustomResource": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "Bucket": Object { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04", + }, + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "CustomResourceProviderframeworkonEvent0AA4376C", + "Arn", + ], + }, + "SourceBucket": "wildrydes-us-east-1", + "SourcePrefix": "WebApplication/1_StaticWebHosting/website/", + }, + "Type": "AWS::CloudFormation::CustomResource", + "UpdateReplacePolicy": "Delete", + }, + "CustomResourceProviderframeworkonEvent0AA4376C": Object { + "DependsOn": Array [ + "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647", + "CustomResourceProviderframeworkonEventServiceRole7EBC5835", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3Bucket6B4B2C9B", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "USER_ON_EVENT_FUNCTION_ARN": Object { + "Fn::GetAtt": Array [ + "copyObjHandlerDA1C4669", + "Arn", + ], + }, + }, + }, + "Handler": "framework.onEvent", + "Role": Object { + "Fn::GetAtt": Array [ + "CustomResourceProviderframeworkonEventServiceRole7EBC5835", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + "Timeout": 900, + }, + "Type": "AWS::Lambda::Function", + }, + "CustomResourceProviderframeworkonEventServiceRole7EBC5835": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "copyObjHandlerDA1C4669", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647", + "Roles": Array [ + Object { + "Ref": "CustomResourceProviderframeworkonEventServiceRole7EBC5835", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "copyObjHandlerDA1C4669": Object { + "DependsOn": Array [ + "copyObjHandlerServiceRoleDefaultPolicyFCA51C18", + "copyObjHandlerServiceRoleA0ECE649", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3BucketC3836BD9", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3VersionKeyEDA18BF9", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3VersionKeyEDA18BF9", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Handler": "copy_s3_objects.on_event", + "Role": Object { + "Fn::GetAtt": Array [ + "copyObjHandlerServiceRoleA0ECE649", + "Arn", + ], + }, + "Runtime": "python3.8", + "Timeout": 300, + }, + "Type": "AWS::Lambda::Function", + }, + "copyObjHandlerServiceRoleA0ECE649": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "copyObjHandlerServiceRoleDefaultPolicyFCA51C18": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject", + "s3:ListBucket", + ], + "Effect": "Allow", + "Resource": Array [ + "arn:aws:s3:::wildrydes-us-east-1", + "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*", + ], + }, + Object { + "Action": Array [ + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl", + "s3:DeleteObject", + "s3:DeleteObjectVersion", + "s3:CopyObject", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:s3:::", + Object { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04", + }, + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:s3:::", + Object { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04", + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "copyObjHandlerServiceRoleDefaultPolicyFCA51C18", + "Roles": Array [ + Object { + "Ref": "copyObjHandlerServiceRoleA0ECE649", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + }, +} +`; diff --git a/source/use_cases/aws-s3-static-website/test/integ.basic-deployment.expected.json b/source/use_cases/aws-s3-static-website/test/integ.basic-deployment.expected.json new file mode 100644 index 000000000..e8e701931 --- /dev/null +++ b/source/use_cases/aws-s3-static-website/test/integ.basic-deployment.expected.json @@ -0,0 +1,622 @@ +{ + "Resources": { + "CloudFrontToS3S3LoggingBucketEF5CD8B2": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + }, + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "CloudFrontToS3S3Bucket9CE6AB04": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "CloudFrontToS3S3LoggingBucketEF5CD8B2" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "CloudFrontToS3S3BucketPolicy2495300D": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::cloudfront:user/CloudFront Origin Access Identity ", + { + "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91" + } + ] + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CloudFrontToS3S3Bucket9CE6AB04", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CloudFrontToS3S3Bucket9CE6AB04", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Principal": { + "CanonicalUser": { + "Fn::GetAtt": [ + "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + "S3CanonicalUserId" + ] + } + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CloudFrontToS3S3Bucket9CE6AB04", + "Arn" + ] + }, + "/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "F16", + "reason": "Public website bucket policy requires a wildcard principal" + } + ] + } + } + }, + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + }, + { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + } + ] + } + } + }, + "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91": { + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + "Properties": { + "CloudFrontOriginAccessIdentityConfig": { + "Comment": "Access S3 bucket content only through CloudFront" + } + } + }, + "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "AllowedMethods": [ + "GET", + "HEAD" + ], + "CachedMethods": [ + "GET", + "HEAD" + ], + "Compress": true, + "ForwardedValues": { + "Cookies": { + "Forward": "none" + }, + "QueryString": false + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https" + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": { + "Bucket": { + "Fn::GetAtt": [ + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + "RegionalDomainName" + ] + }, + "IncludeCookies": false + }, + "Origins": [ + { + "DomainName": { + "Fn::GetAtt": [ + "CloudFrontToS3S3Bucket9CE6AB04", + "RegionalDomainName" + ] + }, + "Id": "origin1", + "S3OriginConfig": { + "OriginAccessIdentity": { + "Fn::Join": [ + "", + [ + "origin-access-identity/cloudfront/", + { + "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91" + } + ] + ] + } + } + } + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": { + "CloudFrontDefaultCertificate": true + } + } + } + }, + "copyObjHandlerServiceRoleA0ECE649": { + "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" + ] + ] + } + ] + } + }, + "copyObjHandlerServiceRoleDefaultPolicyFCA51C18": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject", + "s3:ListBucket" + ], + "Effect": "Allow", + "Resource": [ + "arn:aws:s3:::wildrydes-us-east-1", + "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*" + ] + }, + { + "Action": [ + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl", + "s3:DeleteObject", + "s3:DeleteObjectVersion", + "s3:CopyObject" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "copyObjHandlerServiceRoleDefaultPolicyFCA51C18", + "Roles": [ + { + "Ref": "copyObjHandlerServiceRoleA0ECE649" + } + ] + } + }, + "copyObjHandlerDA1C4669": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3BucketC3836BD9" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3VersionKeyEDA18BF9" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3VersionKeyEDA18BF9" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "copy_s3_objects.on_event", + "Role": { + "Fn::GetAtt": [ + "copyObjHandlerServiceRoleA0ECE649", + "Arn" + ] + }, + "Runtime": "python3.8", + "Timeout": 300 + }, + "DependsOn": [ + "copyObjHandlerServiceRoleDefaultPolicyFCA51C18", + "copyObjHandlerServiceRoleA0ECE649" + ] + }, + "CustomResourceProviderframeworkonEventServiceRole7EBC5835": { + "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" + ] + ] + } + ] + } + }, + "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "copyObjHandlerDA1C4669", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647", + "Roles": [ + { + "Ref": "CustomResourceProviderframeworkonEventServiceRole7EBC5835" + } + ] + } + }, + "CustomResourceProviderframeworkonEvent0AA4376C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3Bucket6B4B2C9B" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "framework.onEvent", + "Role": { + "Fn::GetAtt": [ + "CustomResourceProviderframeworkonEventServiceRole7EBC5835", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "copyObjHandlerDA1C4669", + "Arn" + ] + } + } + }, + "Timeout": 900 + }, + "DependsOn": [ + "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647", + "CustomResourceProviderframeworkonEventServiceRole7EBC5835" + ] + }, + "CustomResource": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomResourceProviderframeworkonEvent0AA4376C", + "Arn" + ] + }, + "SourceBucket": "wildrydes-us-east-1", + "SourcePrefix": "WebApplication/1_StaticWebHosting/website/", + "Bucket": { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + }, + "Parameters": { + "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3BucketC3836BD9": { + "Type": "String", + "Description": "S3 bucket for asset \"2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503deb\"" + }, + "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3VersionKeyEDA18BF9": { + "Type": "String", + "Description": "S3 key for asset version \"2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503deb\"" + }, + "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debArtifactHashA2BE913F": { + "Type": "String", + "Description": "Artifact hash for asset \"2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503deb\"" + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3Bucket6B4B2C9B": { + "Type": "String", + "Description": "S3 bucket for asset \"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\"" + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19": { + "Type": "String", + "Description": "S3 key for asset version \"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\"" + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9ArtifactHash72EE40C1": { + "Type": "String", + "Description": "Artifact hash for asset \"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\"" + } + }, + "Outputs": { + "websiteURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E", + "DomainName" + ] + } + ] + ] + } + } + } +} \ No newline at end of file diff --git a/source/use_cases/aws-s3-static-website/test/integ.basic-deployment.ts b/source/use_cases/aws-s3-static-website/test/integ.basic-deployment.ts new file mode 100644 index 000000000..6e8a10593 --- /dev/null +++ b/source/use_cases/aws-s3-static-website/test/integ.basic-deployment.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from '@aws-cdk/core'; +import { S3StaticWebsiteStack } from '../lib/s3-static-site-stack'; + +const app = new cdk.App(); +new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); \ No newline at end of file diff --git a/source/use_cases/aws-s3-static-website/test/s3-static-site-stack.test.ts b/source/use_cases/aws-s3-static-website/test/s3-static-site-stack.test.ts new file mode 100644 index 000000000..e42365947 --- /dev/null +++ b/source/use_cases/aws-s3-static-website/test/s3-static-site-stack.test.ts @@ -0,0 +1,119 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from '@aws-cdk/core'; +import { S3StaticWebsiteStack } from '../lib/s3-static-site-stack'; +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +test('default stack', () => { + const app = new cdk.App(); + const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check s3 bucket encryption setting', () => { + const app = new cdk.App(); + const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); + expect(stack).toHaveResource("AWS::S3::Bucket", { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + SSEAlgorithm: "AES256" + } + } + ] + } + }); +}); + +test('check s3 bucket public access setting', () => { + const app = new cdk.App(); + const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); + expect(stack).toHaveResource("AWS::S3::Bucket", { + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true + } + }); +}); + +test('check CR lambda function permissions', () => { + const app = new cdk.App(); + const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); + expect(stack).toHaveResource("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "s3:GetObject", + "s3:ListBucket" + ], + Effect: "Allow", + Resource: [ + "arn:aws:s3:::wildrydes-us-east-1", + "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*" + ] + }, + { + Action: [ + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl", + "s3:DeleteObject", + "s3:DeleteObjectVersion", + "s3:CopyObject" + ], + Effect: "Allow", + Resource: [ + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + Ref: "CloudFrontToS3S3Bucket9CE6AB04" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + Ref: "CloudFrontToS3S3Bucket9CE6AB04" + }, + "/*" + ] + ] + } + ] + } + ], + Version: "2012-10-17" + }, + PolicyName: "copyObjHandlerServiceRoleDefaultPolicyFCA51C18", + Roles: [ + { + Ref: "copyObjHandlerServiceRoleA0ECE649" + } + ] + }); +}); \ No newline at end of file diff --git a/source/use_cases/aws-s3-static-website/tsconfig.json b/source/use_cases/aws-s3-static-website/tsconfig.json new file mode 100644 index 000000000..27d10a655 --- /dev/null +++ b/source/use_cases/aws-s3-static-website/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + "charset": "utf8", + "declaration": true, + "experimentalDecorators": true, + "inlineSourceMap": true, + "inlineSources": true, + "lib": [ "es2018" ], + "module": "CommonJS", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "target": "ES2018" + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/source/use_cases/aws-serverless-image-handler/.eslintignore b/source/use_cases/aws-serverless-image-handler/.eslintignore new file mode 100644 index 000000000..f829c06de --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/.eslintignore @@ -0,0 +1,7 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js +lib/lambda/image-handler/**/* +cdk.out \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/.gitignore b/source/use_cases/aws-serverless-image-handler/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/.npmignore b/source/use_cases/aws-serverless-image-handler/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/README.md b/source/use_cases/aws-serverless-image-handler/README.md new file mode 100644 index 000000000..b350f4013 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/README.md @@ -0,0 +1,36 @@ +# AWS Serverless Image Handler + +This use case construct implements an Amazon CloudFront distribution, an Amazon API Gateway REST API, an AWS Lambda +function, and necessary permissions/logic to provision a functional image handler API for serving image content from +one or more Amazon S3 buckets within the deployment account. + +Here is a minimal deployable pattern definition: + +``` +const { ServerlessImageHandler } = require('@aws-konstruk/aws-serverless-image-handler'); + +new ServerlessImageHandler(stack, 'ServerlessImageHandlerPattern', { + deployLambda: true, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.asset(`${__dirname}/lambda`) + } +}); + +``` + +## Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|deployLambda|`boolean`|Whether to create a new Lambda function or use an existing Lambda function. If set to false, you must provide an existing function for the `existingLambdaObj` property.| +|existingLambdaObj?|`lambda.Function`|An optional, existing Lambda function. This property is required if `deployLambda` is set to false.| +|lambdaFunctionProps?|`lambda.FunctionProps`|Optional user-provided props to override the default props for the Lambda function. This property is only required if `deployLambda` is set to true.| +|apiGatewayProps?|`api.LambdaRestApiProps`|Optional user-provided props to override the default props for the API.| + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/architecture.png b/source/use_cases/aws-serverless-image-handler/architecture.png new file mode 100644 index 000000000..4e617ab6e Binary files /dev/null and b/source/use_cases/aws-serverless-image-handler/architecture.png differ diff --git a/source/use_cases/aws-serverless-image-handler/lib/index.ts b/source/use_cases/aws-serverless-image-handler/lib/index.ts new file mode 100644 index 000000000..9047fd8c5 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/lib/index.ts @@ -0,0 +1,243 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import * as defaults from '@aws-solutions-konstruk/core'; +import { Construct } from '@aws-cdk/core'; +import * as cloudFront from '@aws-cdk/aws-cloudfront'; +import * as apiGateway from '@aws-cdk/aws-apigateway'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import { CloudFrontToApiGatewayToLambda } from '@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda'; +import { LambdaToS3 } from '@aws-solutions-konstruk/aws-lambda-s3'; + +/** + * The properties for the ServerlessImageHandler class. + */ +export interface ServerlessImageHandlerProps { + /** + * Whether or not to emable Cross-Origin Resource Sharing (CORS) for the image handler API. + * + * @default - false. + */ + readonly corsEnabled?: boolean + /** + * The CORS origin to use for the image handler API. This property is only required if `corsEnabled` is set to true. + * + * @default - none. + */ + readonly corsOrigin?: string + /** + * One or more buckets within the deployment account to be used for storing/sourcing original image files. + * + * @default - required. + */ + readonly sourceBuckets: string + /** + * The amount of time for CloudWatch log entries from this solution to be retained. + * + * @default - 1 day. + */ + readonly logRetentionPeriod?: number, + /** + * Whether or not to accept/enable automatic WebP based on Accept- headers. + * + * @default - false. + */ + readonly autoWebP?: boolean, + /** + * Optional user provided props to override the default props for each resource. + * + * @default - undefined/optional. + */ + readonly customProps?: ServerlessImageHandlerCustomProps +} + +/** + * Custom properties for the ServerlessImageHandler class. + */ +export interface ServerlessImageHandlerCustomProps { + /** + * Optional user provided props to override the default props for the CloudFront distribution. + * + * @default - false. + */ + readonly cloudFrontDistributionProps?: cloudFront.CloudFrontWebDistributionProps | any, + /** + * Optional user provided props to override the default props for the API Gateway REST API. + * + * @default - none. + */ + readonly apiGatewayProps?: apiGateway.RestApiProps | any, + /** + * Optional user provided props to override the default props for the Lambda function. + * + * @default - none. + */ + readonly lambdaFunctionProps?: lambda.FunctionProps | any, + /** + * Optional user provided props to override the default props for the S3 bucket. + * + * @default - none. + */ + readonly bucketProps?: s3.BucketProps | any, + /** + * Optional user provided props to override the default permissions for the S3 bucket. + * + * @default - none. + */ + readonly bucketPermissions?: string[] +} + +/** + * @summary The ServerlessImageHandler class. + */ +export class ServerlessImageHandler extends Construct { + // Private variables + private cloudFrontApiGatewayLambda: CloudFrontToApiGatewayToLambda; + private lambdaS3: LambdaToS3; + private customProps: any; + + /** + * @summary Constructs a new instance of the ServerlessImageHandler class. + * @param {cdk.App} scope - represents the scope for all the resources. + * @param {string} id - this is a a scope-unique id. + * @param {ServerlessImageHandlerProps} props - user provided props for the construct + * @since 0.8.0 + * @access public + */ + constructor(scope: Construct, id: string, props: ServerlessImageHandlerProps) { + super(scope, id); + + // If customProps is undefined, define it + this.customProps = (props.customProps === undefined) ? {} : props.customProps; + + // Use case specific properties for the Lambda function + const useCaseFunctionProps: lambda.FunctionProps = { + code: lambda.Code.asset(`${__dirname}/lambda/image-handler`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + environment: { + AUTO_WEBP: (props.autoWebP) ? 'Yes' : 'No', + CORS_ENABLED: (props.corsEnabled) ? 'Yes' : 'No', + CORS_ORIGIN: (props.corsOrigin) ? props.corsOrigin : '' + } + }; + const functionProps = (this.customProps.lambdaFunctionProps) ? + defaults.overrideProps(useCaseFunctionProps, this.customProps.lambdaFunctionProps) : useCaseFunctionProps; + + // Use case specific properties for the API Gateway + const useCaseApiProps: apiGateway.RestApiProps = { + binaryMediaTypes: [ "*/*" ] + }; + const apiProps = (this.customProps.apiGatewayProps) ? + defaults.overrideProps(useCaseApiProps, this.customProps.apiGatewayProps) : useCaseApiProps; + + // Build the CloudFrontToApiGatewayToLambda pattern + this.cloudFrontApiGatewayLambda = new CloudFrontToApiGatewayToLambda(this, 'CloudFrontApiGatewayLambda', { + cloudFrontDistributionProps: (this.customProps.cloudFrontDistributionProps) ? this.customProps.cloudFrontDistributionProps : undefined, + apiGatewayProps: apiProps, + deployLambda: true, + lambdaFunctionProps: functionProps + }); + const existingLambdaFn = this.cloudFrontApiGatewayLambda.lambdaFunction(); + + // Build the LambdaToS3 pattern + this.lambdaS3 = new LambdaToS3(this, 'ExistingLambdaS3', { + deployLambda: false, + existingLambdaObj: existingLambdaFn, + bucketProps: this.customProps.bucketProps, + bucketPermissions: (this.customProps.bucketPermissions) ? this.customProps.bucketPermissions : undefined + }); + + // Add additional permissions for Lambda to source original images from any bucket in the account + const lambdaSourcingPolicyStmt = new iam.PolicyStatement({ + effect: iam.Effect.ALLOW + }); + lambdaSourcingPolicyStmt.addResources('arn:aws:s3:::*'); + lambdaSourcingPolicyStmt.addActions('s3:GetObject*', 's3:GetBucket*', 's3:List*'); + + // Add additional permissions for Lambda to access Rekognition services + const lambdaRekognitionPolicyStmt = new iam.PolicyStatement({ + effect: iam.Effect.ALLOW + }); + lambdaRekognitionPolicyStmt.addResources('*'); + lambdaRekognitionPolicyStmt.addActions('rekognition:DetectFaces'); + + // Append the additional permissions to an inline policy + const inlinePolicy = new iam.Policy(this, 'LambdaS3AccessPolicy', { + statements: [ lambdaSourcingPolicyStmt, lambdaRekognitionPolicyStmt ] + }); + + // Add cfn_nag suppression for Rekognition wildcard resource + const rawInlinePolicy: iam.CfnPolicy = inlinePolicy.node.findChild('Resource') as iam.CfnPolicy; + rawInlinePolicy.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [{ + id: 'W12', + reason: `Specified Rekognition action needs wildcard resource.` + }] + } + }; + + // Attach the inline policy to the Lambda function role + existingLambdaFn.role?.attachInlinePolicy(inlinePolicy); + + // Add the SOURCE_BUCKETS environment variable to the Lambda function + const bucketsArr = (props.sourceBuckets !== "") ? props.sourceBuckets.split(',') : []; + bucketsArr.push(this.lambdaS3.s3Bucket().bucketName); + const bucketsStr = bucketsArr.toString().replace(/\s+/g, ''); + this.cloudFrontApiGatewayLambda.lambdaFunction().addEnvironment("SOURCE_BUCKETS", bucketsStr); + } + + /** + * @summary Returns an instance of cloudFront.CloudFrontWebDistribution created by the construct. + * @returns { cloudFront.CloudFrontWebDistribution } Instance of CloudFrontWebDistribution created by the construct. + * @since 0.8.0 + * @access public + */ + public cloudFrontDistribution(): cloudFront.CloudFrontWebDistribution { + return this.cloudFrontApiGatewayLambda.cloudFrontWebDistribution(); + } + + /** + * @summary Returns an instance of apiGateway.RestApi created by the construct. + * @returns { apiGateway.RestApi } Instance of RestApi created by the construct. + * @since 0.8.0 + * @access public + */ + public apiGateway(): apiGateway.RestApi { + return this.cloudFrontApiGatewayLambda.restApi(); + } + + /** + * @summary Returns an instance of lambda.Function created by the construct. + * @returns { lambda.Function } Instance of Function created by the construct + * @since 0.8.0 + * @access public + */ + public lambdaFunction(): lambda.Function { + return this.cloudFrontApiGatewayLambda.lambdaFunction(); + } + + /** + * @summary Returns an instance of s3.Bucket created by the construct. + * @returns { s3.Bucket } Instance of Bucket created by the construct + * @since 0.8.0 + * @access public + */ + public s3Bucket(): s3.Bucket { + return this.lambdaS3.s3Bucket(); + } +} \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/image-handler.js b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/image-handler.js new file mode 100755 index 000000000..49bf4b615 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/image-handler.js @@ -0,0 +1,240 @@ +/********************************************************************************************************************* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * + * with the License. A copy of the License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * + * and limitations under the License. * + *********************************************************************************************************************/ + +const AWS = require('aws-sdk'); +const sharp = require('sharp'); + +class ImageHandler { + + /** + * Main method for processing image requests and outputting modified images. + * @param {ImageRequest} request - An ImageRequest object. + */ + async process(request) { + const originalImage = request.originalImage; + const edits = request.edits; + if (edits !== undefined) { + const modifiedImage = await this.applyEdits(originalImage, edits); + if (request.outputFormat !== undefined) { + modifiedImage.toFormat(request.outputFormat); + } + const bufferImage = await modifiedImage.toBuffer(); + return bufferImage.toString('base64'); + } else { + return originalImage.toString('base64'); + } + } + + /** + * Applies image modifications to the original image based on edits + * specified in the ImageRequest. + * @param {Buffer} originalImage - The original image. + * @param {Object} edits - The edits to be made to the original image. + */ + async applyEdits(originalImage, edits) { + if (edits.resize === undefined) { + edits.resize = {}; + edits.resize.fit = 'inside'; + } + + const image = sharp(originalImage, { failOnError: false }); + const metadata = await image.metadata(); + const keys = Object.keys(edits); + const values = Object.values(edits); + + // Apply the image edits + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = values[i]; + if (key === 'overlayWith') { + let imageMetadata = metadata; + if (edits.resize) { + let imageBuffer = await image.toBuffer(); + imageMetadata = await sharp(imageBuffer).resize({ edits: { resize: edits.resize }}).metadata(); + } + + const { bucket, key, wRatio, hRatio, alpha } = value; + const overlay = await this.getOverlayImage(bucket, key, wRatio, hRatio, alpha, imageMetadata); + const overlayMetadata = await sharp(overlay).metadata(); + + let { options } = value; + if (options) { + if (options.left) { + let left = options.left; + if (left.endsWith('p')) { + left = parseInt(left.replace('p', '')); + if (left < 0) { + left = imageMetadata.width + (imageMetadata.width * left / 100) - overlayMetadata.width; + } else { + left = imageMetadata.width * left / 100; + } + } else { + left = parseInt(left); + if (left < 0) { + left = imageMetadata.width + left - overlayMetadata.width; + } + } + options.left = parseInt(left); + } + if (options.top) { + let top = options.top; + if (top.endsWith('p')) { + top = parseInt(top.replace('p', '')); + if (top < 0) { + top = imageMetadata.height + (imageMetadata.height * top / 100) - overlayMetadata.height; + } else { + top = imageMetadata.height * top / 100; + } + } else { + top = parseInt(top); + if (top < 0) { + top = imageMetadata.height + top - overlayMetadata.height; + } + } + options.top = parseInt(top); + } + } + + const params = [{ ...options, input: overlay }]; + image.composite(params); + } else if (key === 'smartCrop') { + const options = value; + const imageBuffer = await image.toBuffer(); + const boundingBox = await this.getBoundingBox(imageBuffer, options.faceIndex); + const cropArea = this.getCropArea(boundingBox, options, metadata); + try { + image.extract(cropArea) + } catch (err) { + throw ({ + status: 400, + code: 'SmartCrop::PaddingOutOfBounds', + message: 'The padding value you provided exceeds the boundaries of the original image. Please try choosing a smaller value or applying padding via Sharp for greater specificity.' + }); + } + } else { + image[key](value); + } + } + // Return the modified image + return image; + } + + /** + * Gets an image to be used as an overlay to the primary image from an + * Amazon S3 bucket. + * @param {string} bucket - The name of the bucket containing the overlay. + * @param {string} key - The keyname corresponding to the overlay. + */ + async getOverlayImage(bucket, key, wRatio, hRatio, alpha, sourceImageMetadata) { + const s3 = new AWS.S3(); + const params = { Bucket: bucket, Key: key }; + try { + const { width, height } = sourceImageMetadata; + const overlayImage = await s3.getObject(params).promise(); + let resize = { + fit: 'inside' + } + + // Set width and height of the watermark image based on the ratio + const zeroToHundred = /^(100|[1-9]?[0-9])$/; + if (zeroToHundred.test(wRatio)) { + resize['width'] = parseInt(width * wRatio / 100); + } + if (zeroToHundred.test(hRatio)) { + resize['height'] = parseInt(height * hRatio / 100); + } + + // If alpha is not within 0-100, the default alpha is 0 (fully opaque). + if (zeroToHundred.test(alpha)) { + alpha = parseInt(alpha); + } else { + alpha = 0; + } + + const convertedImage = await sharp(overlayImage.Body) + .resize(resize) + .composite([{ + input: Buffer.from([255, 255, 255, 255 * (1 - alpha / 100)]), + raw: { + width: 1, + height: 1, + channels: 4 + }, + tile: true, + blend: 'dest-in' + }]).toBuffer(); + return Promise.resolve(convertedImage); + } catch (err) { + return Promise.reject({ + status: err.statusCode ? err.statusCode : 500, + code: err.code, + message: err.message + }) + } + } + + /** + * Calculates the crop area for a smart-cropped image based on the bounding + * box data returned by Amazon Rekognition, as well as padding options and + * the image metadata. + * @param {Object} boundingBox - The boudning box of the detected face. + * @param {Object} options - Set of options for smart cropping. + * @param {Object} metadata - Sharp image metadata. + */ + getCropArea(boundingBox, options, metadata) { + const padding = (options.padding !== undefined) ? parseFloat(options.padding) : 0; + // Calculate the smart crop area + const cropArea = { + left : parseInt((boundingBox.Left*metadata.width)-padding), + top : parseInt((boundingBox.Top*metadata.height)-padding), + width : parseInt((boundingBox.Width*metadata.width)+(padding*2)), + height : parseInt((boundingBox.Height*metadata.height)+(padding*2)), + } + // Return the crop area + return cropArea; + } + + /** + * Gets the bounding box of the specified face index within an image, if specified. + * @param {Sharp} imageBuffer - The original image. + * @param {Integer} faceIndex - The zero-based face index value, moving from 0 and up as + * confidence decreases for detected faces within the image. + */ + async getBoundingBox(imageBuffer, faceIndex) { + const rekognition = new AWS.Rekognition(); + const params = { Image: { Bytes: imageBuffer }}; + const faceIdx = (faceIndex !== undefined) ? faceIndex : 0; + try { + const response = await rekognition.detectFaces(params).promise(); + return Promise.resolve(response.FaceDetails[faceIdx].BoundingBox); + } catch (err) { + console.log(err); + if (err.message === "Cannot read property 'BoundingBox' of undefined") { + return Promise.reject({ + status: 400, + code: 'SmartCrop::FaceIndexOutOfRange', + message: 'You have provided a FaceIndex value that exceeds the length of the zero-based detectedFaces array. Please specify a value that is in-range.' + }) + } else { + return Promise.reject({ + status: 500, + code: err.code, + message: err.message + }) + } + } + } +} + +// Exports +module.exports = ImageHandler; diff --git a/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/image-request.js b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/image-request.js new file mode 100755 index 000000000..f8224543a --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/image-request.js @@ -0,0 +1,305 @@ +/********************************************************************************************************************* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * + * with the License. A copy of the License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * + * and limitations under the License. * + *********************************************************************************************************************/ + +const ThumborMapping = require('./thumbor-mapping'); + +class ImageRequest { + + /** + * Initializer function for creating a new image request, used by the image + * handler to perform image modifications. + * @param {Object} event - Lambda request body. + */ + async setup(event) { + try { + this.requestType = this.parseRequestType(event); + this.bucket = this.parseImageBucket(event, this.requestType); + this.key = this.parseImageKey(event, this.requestType); + this.edits = this.parseImageEdits(event, this.requestType); + this.originalImage = await this.getOriginalImage(this.bucket, this.key); + + /* Decide the output format of the image. + * 1) If the format is provided, the output format is the provided format. + * 2) If headers contain "Accept: image/webp", the output format is webp. + * 3) Use the default image format for the rest of cases. + */ + let outputFormat = this.getOutputFormat(event); + if (this.edits && this.edits.toFormat) { + this.outputFormat = this.edits.toFormat; + } else if (outputFormat) { + this.outputFormat = outputFormat; + } + + // Fix quality for Thumbor and Custom request type if outputFormat is different from quality type. + if (this.outputFormat) { + const requestType = ['Custom', 'Thumbor']; + const acceptedValues = ['jpeg', 'png', 'webp', 'tiff', 'heif']; + + this.ContentType = `image/${this.outputFormat}`; + if (requestType.includes(this.requestType) && acceptedValues.includes(this.outputFormat)) { + let qualityKey = Object.keys(this.edits).filter(key => acceptedValues.includes(key))[0]; + if (qualityKey && (qualityKey !== this.outputFormat)) { + const qualityValue = this.edits[qualityKey]; + this.edits[this.outputFormat] = qualityValue; + delete this.edits[qualityKey]; + } + } + } + + return Promise.resolve(this); + + } catch (err) { + return Promise.reject(err); + } + } + + /** + * Gets the original image from an Amazon S3 bucket. + * @param {String} bucket - The name of the bucket containing the image. + * @param {String} key - The key name corresponding to the image. + * @return {Promise} - The original image or an error. + */ + async getOriginalImage(bucket, key) { + const S3 = require('aws-sdk/clients/s3'); + const s3 = new S3(); + const imageLocation = { Bucket: bucket, Key: key }; + try { + const originalImage = await s3.getObject(imageLocation).promise(); + + if (originalImage.ContentType) { + this.ContentType = originalImage.ContentType; + } else { + this.ContentType = "image"; + } + + if (originalImage.Expires) { + this.Expires = new Date(originalImage.Expires).toUTCString(); + } + + if (originalImage.LastModified) { + this.LastModified = new Date(originalImage.LastModified).toUTCString(); + } + + if (originalImage.CacheControl) { + this.CacheControl = originalImage.CacheControl; + } else { + this.CacheControl = "max-age=31536000,public"; + } + + return Promise.resolve(originalImage.Body); + } catch(err) { + return Promise.reject({ + status: ('NoSuchKey' === err.code) ? 404 : 500, + code: err.code, + message: err.message + }); + } + } + + /** + * Parses the name of the appropriate Amazon S3 bucket to source the + * original image from. + * @param {String} event - Lambda request body. + * @param {String} requestType - Image handler request type. + */ + parseImageBucket(event, requestType) { + if (requestType === "Default") { + // Decode the image request + const decoded = this.decodeRequest(event); + if (decoded.bucket !== undefined) { + // Check the provided bucket against the whitelist + const sourceBuckets = this.getAllowedSourceBuckets(); + if (sourceBuckets.includes(decoded.bucket) || decoded.bucket.match(new RegExp('^' + sourceBuckets[0] + '$'))) { + return decoded.bucket; + } else { + throw ({ + status: 403, + code: 'ImageBucket::CannotAccessBucket', + message: 'The bucket you specified could not be accessed. Please check that the bucket is specified in your SOURCE_BUCKETS.' + }); + } + } else { + // Try to use the default image source bucket env var + const sourceBuckets = this.getAllowedSourceBuckets(); + return sourceBuckets[0]; + } + } else if (requestType === "Thumbor" || requestType === "Custom") { + // Use the default image source bucket env var + const sourceBuckets = this.getAllowedSourceBuckets(); + return sourceBuckets[0]; + } else { + throw ({ + status: 404, + code: 'ImageBucket::CannotFindBucket', + message: 'The bucket you specified could not be found. Please check the spelling of the bucket name in your request.' + }); + } + } + + /** + * Parses the edits to be made to the original image. + * @param {String} event - Lambda request body. + * @param {String} requestType - Image handler request type. + */ + parseImageEdits(event, requestType) { + if (requestType === "Default") { + const decoded = this.decodeRequest(event); + return decoded.edits; + } else if (requestType === "Thumbor") { + const thumborMapping = new ThumborMapping(); + thumborMapping.process(event); + return thumborMapping.edits; + } else if (requestType === "Custom") { + const thumborMapping = new ThumborMapping(); + const parsedPath = thumborMapping.parseCustomPath(event.path); + thumborMapping.process(parsedPath); + return thumborMapping.edits; + } else { + throw ({ + status: 400, + code: 'ImageEdits::CannotParseEdits', + message: 'The edits you provided could not be parsed. Please check the syntax of your request and refer to the documentation for additional guidance.' + }); + } + } + + /** + * Parses the name of the appropriate Amazon S3 key corresponding to the + * original image. + * @param {String} event - Lambda request body. + * @param {String} requestType - Type, either "Default", "Thumbor", or "Custom". + */ + parseImageKey(event, requestType) { + if (requestType === "Default") { + // Decode the image request and return the image key + const decoded = this.decodeRequest(event); + return decoded.key; + } + + if (requestType === "Thumbor" || requestType === "Custom") { + return decodeURIComponent(event["path"].replace(/\d+x\d+\/|filters[:-][^/;]+|\/fit-in\/+|^\/+/g,'').replace(/^\/+/,'')); + } + + // Return an error for all other conditions + throw ({ + status: 404, + code: 'ImageEdits::CannotFindImage', + message: 'The image you specified could not be found. Please check your request syntax as well as the bucket you specified to ensure it exists.' + }); + } + + /** + * Determines how to handle the request being made based on the URL path + * prefix to the image request. Categorizes a request as either "image" + * (uses the Sharp library), "thumbor" (uses Thumbor mapping), or "custom" + * (uses the rewrite function). + * @param {Object} event - Lambda request body. + */ + parseRequestType(event) { + const path = event["path"]; + // ---- + const matchDefault = new RegExp(/^(\/?)([0-9a-zA-Z+\/]{4})*(([0-9a-zA-Z+\/]{2}==)|([0-9a-zA-Z+\/]{3}=))?$/); + const matchThumbor = new RegExp(/^(\/?)((fit-in)?|(filters:.+\(.?\))?|(unsafe)?).*(.+jpg|.+png|.+webp|.+tiff|.+jpeg)$/i); + const matchCustom = new RegExp(/(\/?)(.*)(jpg|png|webp|tiff|jpeg)/i); + const definedEnvironmentVariables = ( + (process.env.REWRITE_MATCH_PATTERN !== "") && + (process.env.REWRITE_SUBSTITUTION !== "") && + (process.env.REWRITE_MATCH_PATTERN !== undefined) && + (process.env.REWRITE_SUBSTITUTION !== undefined) + ); + // ---- + console.log(path); + if (matchDefault.test(path)) { // use sharp + return 'Default'; + } else if (matchCustom.test(path) && definedEnvironmentVariables) { // use rewrite function then thumbor mappings + return 'Custom'; + } else if (matchThumbor.test(path)) { // use thumbor mappings + return 'Thumbor'; + } else { + throw { + status: 400, + code: 'RequestTypeError', + message: 'The type of request you are making could not be processed. Please ensure that your original image is of a supported file type (jpg, png, tiff, webp) and that your image request is provided in the correct syntax. Refer to the documentation for additional guidance on forming image requests.' + }; + } + } + + /** + * Decodes the base64-encoded image request path associated with default + * image requests. Provides error handling for invalid or undefined path values. + * @param {Object} event - The proxied request object. + */ + decodeRequest(event) { + const path = event["path"]; + if (path !== undefined) { + const splitPath = path.split("/"); + const encoded = splitPath[splitPath.length - 1]; + const toBuffer = Buffer.from(encoded, 'base64'); + try { + // To support European characters, 'ascii' was removed. + return JSON.parse(toBuffer.toString()); + } catch (e) { + throw ({ + status: 400, + code: 'DecodeRequest::CannotDecodeRequest', + message: 'The image request you provided could not be decoded. Please check that your request is base64 encoded properly and refer to the documentation for additional guidance.' + }); + } + } else { + throw ({ + status: 400, + code: 'DecodeRequest::CannotReadPath', + message: 'The URL path you provided could not be read. Please ensure that it is properly formed according to the solution documentation.' + }); + } + } + + /** + * Returns a formatted image source bucket whitelist as specified in the + * SOURCE_BUCKETS environment variable of the image handler Lambda + * function. Provides error handling for missing/invalid values. + */ + getAllowedSourceBuckets() { + const sourceBuckets = process.env.SOURCE_BUCKETS; + if (sourceBuckets === undefined) { + throw ({ + status: 400, + code: 'GetAllowedSourceBuckets::NoSourceBuckets', + message: 'The SOURCE_BUCKETS variable could not be read. Please check that it is not empty and contains at least one source bucket, or multiple buckets separated by commas. Spaces can be provided between commas and bucket names, these will be automatically parsed out when decoding.' + }); + } else { + const formatted = sourceBuckets.replace(/\s+/g, ''); + const buckets = formatted.split(','); + return buckets; + } + } + + /** + * Return the output format depending on the accepts headers and request type + * @param {Object} event - The request body. + */ + getOutputFormat(event) { + const autoWebP = process.env.AUTO_WEBP; + if (autoWebP && event.headers.Accept && event.headers.Accept.includes('image/webp')) { + return 'webp'; + } else if (this.requestType === 'Default') { + const decoded = this.decodeRequest(event); + return decoded.outputFormat; + } + + return null; + } +} + +// Exports +module.exports = ImageRequest; \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/index.js b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/index.js new file mode 100755 index 000000000..1f1ca8c08 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/index.js @@ -0,0 +1,65 @@ +/********************************************************************************************************************* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * + * with the License. A copy of the License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * + * and limitations under the License. * + *********************************************************************************************************************/ + +const ImageRequest = require('./image-request.js'); +const ImageHandler = require('./image-handler.js'); + +exports.handler = async (event) => { + const imageRequest = new ImageRequest(); + const imageHandler = new ImageHandler(); + try { + const request = await imageRequest.setup(event); + const processedRequest = await imageHandler.process(request); + + const headers = getResponseHeaders(); + headers["Content-Type"] = request.ContentType; + headers["Expires"] = request.Expires; + headers["Last-Modified"] = request.LastModified; + headers["Cache-Control"] = request.CacheControl; + + return { + "statusCode": 200, + "headers" : headers, + "body": processedRequest, + "isBase64Encoded": true + }; + } catch (err) { + return { + "statusCode": err.status, + "headers" : getResponseHeaders(true), + "body": JSON.stringify(err), + "isBase64Encoded": false + }; + } +} + +/** + * Generates the appropriate set of response headers based on a success + * or error condition. + * @param {boolean} isErr - has an error been thrown? + */ +const getResponseHeaders = (isErr) => { + const corsEnabled = (process.env.CORS_ENABLED === "Yes"); + const headers = { + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + "Access-Control-Allow-Credentials": true + } + if (corsEnabled) { + headers["Access-Control-Allow-Origin"] = process.env.CORS_ORIGIN; + } + if (isErr) { + headers["Content-Type"] = "application/json" + } + return headers; +} \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/package.json b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/package.json new file mode 100755 index 000000000..290ed7b85 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/package.json @@ -0,0 +1,31 @@ +{ + "name": "image-handler", + "description": "A Lambda function for performing on-demand image edits and manipulations.", + "main": "index.js", + "author": { + "name": "aws-solutions-builder" + }, + "version": "0.0.1", + "private": true, + "dependencies": { + "sharp": "^0.23.4", + "color": "3.1.2", + "color-name": "1.1.4" + }, + "devDependencies": { + "aws-sdk": "^2.437.0", + "aws-sdk-mock": "^4.4.0", + "mocha": "^6.1.4", + "sinon": "^7.3.2", + "nyc": "^14.0.0" + }, + "scripts": { + "pretest": "npm run build:init && npm install", + "test": "nyc --reporter=html --reporter=text mocha", + "build:init": "rm -rf package-lock.json && rm -rf lambda_dist && rm -rf node_modules", + "build:zip": "zip -rq image-handler.zip .", + "build:dist": "mkdir lambda_dist && mv image-handler.zip lambda_dist/", + "build": "npm run build:init && npm install --arch=x64 --platform=linux --production && npm run build:zip && npm run build:dist" + }, + "license": "Apache-2.0" +} diff --git a/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/test/test-image-handler.js b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/test/test-image-handler.js new file mode 100755 index 000000000..a42c2a918 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/test/test-image-handler.js @@ -0,0 +1,476 @@ +/********************************************************************************************************************* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * + * with the License. A copy of the License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * + * and limitations under the License. * + *********************************************************************************************************************/ + +const ImageHandler = require('../image-handler'); +const sharp = require('sharp'); +let assert = require('assert'); + +// ---------------------------------------------------------------------------- +// [async] process() +// ---------------------------------------------------------------------------- +describe('process()', function() { + describe('001/default', function() { + it(`Should pass if the output image is different from the input image with edits applied`, async function() { + // Arrange + const sinon = require('sinon'); + // ---- Amazon S3 stub + const S3 = require('aws-sdk/clients/s3'); + const getObject = S3.prototype.getObject = sinon.stub(); + getObject.returns({ + promise: () => { return { + Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') + }} + }) + // ---- + const request = { + requestType: "default", + bucket: "sample-bucket", + key: "sample-image-001.jpg", + edits: { + grayscale: true, + flip: true + }, + originalImage: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') + } + // Act + const imageHandler = new ImageHandler(); + const result = await imageHandler.process(request); + // Assert + assert.deepEqual((request.originalImage !== result), true); + }); + }); + describe('002/withToFormat', function() { + it(`Should pass if the output image is in a different format than the original image`, async function() { + // Arrange + const sinon = require('sinon'); + // ---- Amazon S3 stub + const S3 = require('aws-sdk/clients/s3'); + const getObject = S3.prototype.getObject = sinon.stub(); + getObject.returns({ + promise: () => { return { + Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') + }} + }) + // ---- + const request = { + requestType: "default", + bucket: "sample-bucket", + key: "sample-image-001.jpg", + outputFormat: "png", + edits: { + grayscale: true, + flip: true + }, + originalImage: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') + } + // Act + const imageHandler = new ImageHandler(); + const result = await imageHandler.process(request); + // Assert + assert.deepEqual((request.originalImage !== result), true); + }); + }); + describe('003/noEditsSpecified', function() { + it(`Should pass if no edits are specified and the original image is returned`, async function() { + // Arrange + const sinon = require('sinon'); + // ---- Amazon S3 stub + const S3 = require('aws-sdk/clients/s3'); + const getObject = S3.prototype.getObject = sinon.stub(); + getObject.returns({ + promise: () => { return { + Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') + }} + }) + // ---- + const request = { + requestType: "default", + bucket: "sample-bucket", + key: "sample-image-001.jpg", + originalImage: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') + } + // Act + const imageHandler = new ImageHandler(); + const result = await imageHandler.process(request); + // Assert + assert.deepEqual(result, 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='); + }); + }); +}); + +// ---------------------------------------------------------------------------- +// [async] applyEdits() +// ---------------------------------------------------------------------------- +describe('applyEdits()', function() { + describe('001/standardEdits', function() { + it(`Should pass if a series of standard edits are provided to the + function`, async function() { + // Arrange + const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); + const edits = { + grayscale: true, + flip: true + } + // Act + const imageHandler = new ImageHandler(); + const result = await imageHandler.applyEdits(originalImage, edits); + // Assert + const expectedResult1 = (result.options.greyscale); + const expectedResult2 = (result.options.flip); + const combinedResults = (expectedResult1 && expectedResult2); + assert.deepEqual(combinedResults, true); + }); + }); + describe('002/overlay', function() { + it(`Should pass if an edit with the overlayWith keyname is passed to + the function`, async function() { + // Arrange + const sinon = require('sinon'); + // ---- Amazon S3 stub + const S3 = require('aws-sdk/clients/s3'); + const getObject = S3.prototype.getObject = sinon.stub(); + getObject.returns({ + promise: () => { return { + Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') + }} + }) + // Act + const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); + const edits = { + overlayWith: { + bucket: 'aaa', + key: 'bbb' + } + } + // Assert + const imageHandler = new ImageHandler(); + await imageHandler.applyEdits(originalImage, edits).then((result) => { + assert.deepEqual(result.options.input.buffer, originalImage); + }); + }); + }); + describe('003/smartCrop', function() { + it(`Should pass if an edit with the smartCrop keyname is passed to + the function`, async function() { + // Arrange + const sinon = require('sinon'); + // ---- Amazon Rekognition stub + const rekognition = require('aws-sdk/clients/rekognition'); + const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); + detectFaces.returns({ + promise: () => { return { + FaceDetails: [{ + BoundingBox: { + Height: 0.18, + Left: 0.55, + Top: 0.33, + Width: 0.23 + } + }] + }} + }) + // Act + const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); + const edits = { + smartCrop: { + faceIndex: 0, + padding: 0 + } + } + // Assert + const imageHandler = new ImageHandler(); + await imageHandler.applyEdits(originalImage, edits).then((result) => { + //console.log(result); + const sharp = require('sharp'); + const originalImageData = sharp(originalImage); + assert.deepEqual((originalImageData.options.input !== result.options.input), true) + }).catch((err) => { + console.log(err) + }) + }); + }); + describe('004/smartCrop/paddingOutOfBoundsError', function() { + it(`Should pass if an excessive padding value is passed to the + smartCrop filter`, async function() { + // Arrange + const sinon = require('sinon'); + // ---- Amazon Rekognition stub + const rekognition = require('aws-sdk/clients/rekognition'); + const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); + detectFaces.returns({ + promise: () => { return { + FaceDetails: [{ + BoundingBox: { + Height: 0.18, + Left: 0.55, + Top: 0.33, + Width: 0.23 + } + }] + }} + }) + // Act + const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); + const edits = { + smartCrop: { + faceIndex: 0, + padding: 80 + } + } + // Assert + const imageHandler = new ImageHandler(); + await imageHandler.applyEdits(originalImage, edits).then((result) => { + //console.log(result); + const sharp = require('sharp'); + const originalImageData = sharp(originalImage); + assert.deepEqual((originalImageData.options.input !== result.options.input), true) + }).catch((err) => { + console.log(err) + }) + }); + }); + describe('005/smartCrop/boundingBoxError', function() { + it(`Should pass if an excessive faceIndex value is passed to the + smartCrop filter`, async function() { + // Arrange + const sinon = require('sinon'); + // ---- Amazon Rekognition stub + const rekognition = require('aws-sdk/clients/rekognition'); + const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); + detectFaces.returns({ + promise: () => { return { + FaceDetails: [{ + BoundingBox: { + Height: 0.18, + Left: 0.55, + Top: 0.33, + Width: 0.23 + } + }] + }} + }) + // Act + const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); + const edits = { + smartCrop: { + faceIndex: 10, + padding: 0 + } + } + // Assert + const imageHandler = new ImageHandler(); + await imageHandler.applyEdits(originalImage, edits).then((result) => { + //console.log(result); + const sharp = require('sharp'); + const originalImageData = sharp(originalImage); + assert.deepEqual((originalImageData.options.input !== result.options.input), true) + }).catch((err) => { + console.log(err) + }) + }); + }); + describe('006/smartCrop/faceIndexUndefined', function() { + it(`Should pass if a faceIndex value of undefined is passed to the + smartCrop filter`, async function() { + // Arrange + const sinon = require('sinon'); + // ---- Amazon Rekognition stub + const rekognition = require('aws-sdk/clients/rekognition'); + const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); + detectFaces.returns({ + promise: () => { return { + FaceDetails: [{ + BoundingBox: { + Height: 0.18, + Left: 0.55, + Top: 0.33, + Width: 0.23 + } + }] + }} + }) + // Act + const originalImage = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'); + const edits = { + smartCrop: true + } + // Assert + const imageHandler = new ImageHandler(); + await imageHandler.applyEdits(originalImage, edits).then((result) => { + //console.log(result); + const sharp = require('sharp'); + const originalImageData = sharp(originalImage); + assert.deepEqual((originalImageData.options.input !== result.options.input), true) + }).catch((err) => { + console.log(err) + }) + }); + }); +}); + +// ---------------------------------------------------------------------------- +// [async] getOverlayImage() +// ---------------------------------------------------------------------------- +describe('getOverlayImage()', function() { + describe('001/validParameters', function() { + it(`Should pass if the proper bucket name and key are supplied, + simulating an image file that can be retrieved`, async function() { + // Arrange + const S3 = require('aws-sdk/clients/s3'); + const sinon = require('sinon'); + const getObject = S3.prototype.getObject = sinon.stub(); + getObject.withArgs({Bucket: 'validBucket', Key: 'validKey'}).returns({ + promise: () => { return { + Body: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') + }} + }) + // Act + const imageHandler = new ImageHandler(); + const metadata = await sharp(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64')).metadata(); + const result = await imageHandler.getOverlayImage('validBucket', 'validKey', '100', '100', '20', metadata); + // Assert + assert.deepEqual(result, Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAsSAAALEgHS3X78AAAADUlEQVQI12P4z8CQCgAEZgFlTg0nBwAAAABJRU5ErkJggg==', 'base64')); + }); + }); + describe('002/imageDoesNotExist', async function() { + it(`Should throw an error if an invalid bucket or key name is provided, + simulating a non-existant overlay image`, async function() { + // Arrange + const S3 = require('aws-sdk/clients/s3'); + const sinon = require('sinon'); + const getObject = S3.prototype.getObject = sinon.stub(); + getObject.withArgs({Bucket: 'invalidBucket', Key: 'invalidKey'}).returns({ + promise: () => { + return Promise.reject({ + code: 500, + message: 'SimulatedInvalidParameterException' + }) + } + }); + // Act + const imageHandler = new ImageHandler(); + // Assert + imageHandler.getOverlayImage('invalidBucket', 'invalidKey').then((result) => { + assert.equal(typeof result, Error); + }).catch((err) => { + console.log(err) + }) + }); + }); +}); + +// ---------------------------------------------------------------------------- +// [async] getCropArea() +// ---------------------------------------------------------------------------- +describe('getCropArea()', function() { + describe('001/validParameters', function() { + it(`Should pass if the crop area can be calculated using a series of + valid inputs/parameters`, function() { + // Arrange + const boundingBox = { + Height: 0.18, + Left: 0.55, + Top: 0.33, + Width: 0.23 + }; + const options = { padding: 20 }; + const metadata = { + width: 200, + height: 400 + }; + // Act + const imageHandler = new ImageHandler(); + const result = imageHandler.getCropArea(boundingBox, options, metadata); + // Assert + const expectedResult = { + left: 90, + top: 112, + width: 86, + height: 112 + } + assert.deepEqual(result, expectedResult); + }); + }); +}); + + +// ---------------------------------------------------------------------------- +// [async] getBoundingBox() +// ---------------------------------------------------------------------------- +describe('getBoundingBox()', function() { + describe('001/validParameters', function() { + it(`Should pass if the proper parameters are passed to the function`, + async function() { + // Arrange + const sinon = require('sinon'); + const rekognition = require('aws-sdk/clients/rekognition'); + const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); + // ---- + const imageBytes = Buffer.from('TestImageData'); + detectFaces.withArgs({Image: {Bytes: imageBytes}}).returns({ + promise: () => { return { + FaceDetails: [{ + BoundingBox: { + Height: 0.18, + Left: 0.55, + Top: 0.33, + Width: 0.23 + } + }] + }} + }) + // ---- + const currentImage = imageBytes; + const faceIndex = 0; + // Act + const imageHandler = new ImageHandler(); + const result = await imageHandler.getBoundingBox(currentImage, faceIndex); + // Assert + const expectedResult = { + Height: 0.18, + Left: 0.55, + Top: 0.33, + Width: 0.23 + }; + assert.deepEqual(result, expectedResult); + }); + }); + describe('002/errorHandling', function() { + it(`Should simulate an error condition returned by Rekognition`, + async function() { + // Arrange + const rekognition = require('aws-sdk/clients/rekognition'); + const sinon = require('sinon'); + const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); + detectFaces.returns({ + promise: () => { + return Promise.reject({ + code: 500, + message: 'SimulatedError' + }) + } + }) + // ---- + const currentImage = Buffer.from('NotTestImageData'); + const faceIndex = 0; + // Act + const imageHandler = new ImageHandler(); + // Assert + imageHandler.getBoundingBox(currentImage, faceIndex).then((result) => { + assert.equal(typeof result, Error); + }).catch((err) => { + console.log(err) + }) + }); + }); +}); \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/test/test-image-request.js b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/test/test-image-request.js new file mode 100755 index 000000000..9507dacef --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/test/test-image-request.js @@ -0,0 +1,757 @@ +/********************************************************************************************************************* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * + * with the License. A copy of the License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * + * and limitations under the License. * + *********************************************************************************************************************/ + +const ImageRequest = require('../image-request'); +let assert = require('assert'); + +// ---------------------------------------------------------------------------- +// [async] setup() +// ---------------------------------------------------------------------------- +describe('setup()', function() { + describe('001/defaultImageRequest', function() { + it(`Should pass when a default image request is provided and populate + the ImageRequest object with the proper values`, async function() { + // Arrange + const event = { + path : '/eyJidWNrZXQiOiJ2YWxpZEJ1Y2tldCIsImtleSI6InZhbGlkS2V5IiwiZWRpdHMiOnsiZ3JheXNjYWxlIjp0cnVlfSwib3V0cHV0Rm9ybWF0IjoianBlZyJ9' + } + process.env = { + SOURCE_BUCKETS : "validBucket, validBucket2" + } + // ---- + const S3 = require('aws-sdk/clients/s3'); + const sinon = require('sinon'); + const getObject = S3.prototype.getObject = sinon.stub(); + getObject.withArgs({Bucket: 'validBucket', Key: 'validKey'}).returns({ + promise: () => { return { + Body: Buffer.from('SampleImageContent\n') + }} + }) + // Act + const imageRequest = new ImageRequest(); + await imageRequest.setup(event); + const expectedResult = { + requestType: 'Default', + bucket: 'validBucket', + key: 'validKey', + edits: { grayscale: true }, + outputFormat: 'jpeg', + originalImage: Buffer.from('SampleImageContent\n'), + CacheControl: 'max-age=31536000,public', + ContentType: 'image/jpeg' + } + // Assert + assert.deepEqual(imageRequest, expectedResult); + }); + }); + describe('002/thumborImageRequest', function() { + it(`Should pass when a thumbor image request is provided and populate + the ImageRequest object with the proper values`, async function() { + // Arrange + const event = { + path : "/filters:grayscale()/test-image-001.jpg" + } + process.env = { + SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" + } + // ---- + const S3 = require('aws-sdk/clients/s3'); + const sinon = require('sinon'); + const getObject = S3.prototype.getObject = sinon.stub(); + getObject.withArgs({Bucket: 'allowedBucket001', Key: 'test-image-001.jpg'}).returns({ + promise: () => { return { + Body: Buffer.from('SampleImageContent\n') + }} + }) + // Act + const imageRequest = new ImageRequest(); + await imageRequest.setup(event); + const expectedResult = { + requestType: 'Thumbor', + bucket: 'allowedBucket001', + key: 'test-image-001.jpg', + edits: { grayscale: true }, + originalImage: Buffer.from('SampleImageContent\n'), + CacheControl: 'max-age=31536000,public', + ContentType: 'image' + } + // Assert + assert.deepEqual(imageRequest, expectedResult); + }); + }); + describe('003/customImageRequest', function() { + it(`Should pass when a custom image request is provided and populate + the ImageRequest object with the proper values`, async function() { + // Arrange + const event = { + path : '/filters-rotate(90)/filters-grayscale()/custom-image.jpg' + } + process.env = { + SOURCE_BUCKETS : "allowedBucket001, allowedBucket002", + REWRITE_MATCH_PATTERN: /(filters-)/gm, + REWRITE_SUBSTITUTION: 'filters:' + } + // ---- + const S3 = require('aws-sdk/clients/s3'); + const sinon = require('sinon'); + const getObject = S3.prototype.getObject = sinon.stub(); + getObject.withArgs({Bucket: 'allowedBucket001', Key: 'custom-image.jpg'}).returns({ + promise: () => { return { + CacheControl: 'max-age=300,public', + ContentType: 'custom-type', + Expires: 'Tue, 24 Dec 2019 13:46:28 GMT', + LastModified: 'Sat, 19 Dec 2009 16:30:47 GMT', + Body: Buffer.from('SampleImageContent\n') + }} + }) + // Act + const imageRequest = new ImageRequest(); + await imageRequest.setup(event); + const expectedResult = { + requestType: 'Custom', + bucket: 'allowedBucket001', + key: 'custom-image.jpg', + edits: { + grayscale: true, + rotate: 90 + }, + originalImage: Buffer.from('SampleImageContent\n'), + CacheControl: 'max-age=300,public', + ContentType: 'custom-type', + Expires: 'Tue, 24 Dec 2019 13:46:28 GMT', + LastModified: 'Sat, 19 Dec 2009 16:30:47 GMT', + } + // Assert + assert.deepEqual(imageRequest, expectedResult); + }); + }); + describe('004/errorCase', function() { + it(`Should pass when an error is caught`, async function() { + // Assert + const event = { + path : '/eyJidWNrZXQiOiJ2YWxpZEJ1Y2tldCIsImtleSI6InZhbGlkS2V5IiwiZWRpdHMiOnsiZ3JheXNjYWxlIjp0cnVlfX0=' + } + // ---- + const S3 = require('aws-sdk/clients/s3'); + const sinon = require('sinon'); + const getObject = S3.prototype.getObject = sinon.stub(); + getObject.withArgs({Bucket: 'validBucket', Key: 'validKey'}).returns({ + promise: () => { return { + Body: Buffer.from('SampleImageContent\n') + }} + }) + // Act + const imageRequest = new ImageRequest(); + // Assert + await imageRequest.setup(event).then(() => { + console.log(data); + }).catch((err) => { + console.log(err); + assert.deepEqual(err.code, 'ImageBucket::CannotAccessBucket'); + }) + }); + }); +}); +// ---------------------------------------------------------------------------- +// getOriginalImage() +// ---------------------------------------------------------------------------- +describe('getOriginalImage()', function() { + describe('001/imageExists', function() { + it(`Should pass if the proper bucket name and key are supplied, + simulating an image file that can be retrieved`, async function() { + // Arrange + const S3 = require('aws-sdk/clients/s3'); + const sinon = require('sinon'); + const getObject = S3.prototype.getObject = sinon.stub(); + getObject.withArgs({Bucket: 'validBucket', Key: 'validKey'}).returns({ + promise: () => { return { + Body: Buffer.from('SampleImageContent\n') + }} + }) + // Act + const imageRequest = new ImageRequest(); + const result = await imageRequest.getOriginalImage('validBucket', 'validKey'); + // Assert + assert.deepEqual(result, Buffer.from('SampleImageContent\n')); + }); + }); + describe('002/imageDoesNotExist', async function() { + it(`Should throw an error if an invalid bucket or key name is provided, + simulating a non-existant original image`, async function() { + // Arrange + const S3 = require('aws-sdk/clients/s3'); + const sinon = require('sinon'); + const getObject = S3.prototype.getObject = sinon.stub(); + getObject.withArgs({Bucket: 'invalidBucket', Key: 'invalidKey'}).returns({ + promise: () => { + return Promise.reject({ + code: 'NoSuchKey', + message: 'SimulatedException' + }) + } + }); + // Act + const imageRequest = new ImageRequest(); + // Assert + imageRequest.getOriginalImage('invalidBucket', 'invalidKey').then((result) => { + assert.equal(typeof result, Error); + assert.equal(result.status, 404); + }).catch((err) => console.log(err)); + }); + }); + describe('003/unknownError', async function() { + it(`Should throw an error if an unkown problem happens when getting an object`, async function() { + // Arrange + const S3 = require('aws-sdk/clients/s3'); + const sinon = require('sinon'); + const getObject = S3.prototype.getObject = sinon.stub(); + getObject.withArgs({Bucket: 'invalidBucket', Key: 'invalidKey'}).returns({ + promise: () => { + return Promise.reject({ + code: 'InternalServerError', + message: 'SimulatedException' + }) + } + }); + // Act + const imageRequest = new ImageRequest(); + // Assert + imageRequest.getOriginalImage('invalidBucket', 'invalidKey').then((result) => { + assert.equal(typeof result, Error); + assert.equal(result.status, 500); + }).catch((err) => console.log(err)); + }); + }); +}); + +// ---------------------------------------------------------------------------- +// parseImageBucket() +// ---------------------------------------------------------------------------- +describe('parseImageBucket()', function() { + describe('001/defaultRequestType/bucketSpecifiedInRequest/allowed', function() { + it(`Should pass if the bucket name is provided in the image request + and has been whitelisted in SOURCE_BUCKETS`, function() { + // Arrange + const event = { + path : '/eyJidWNrZXQiOiJhbGxvd2VkQnVja2V0MDAxIiwia2V5Ijoic2FtcGxlSW1hZ2VLZXkwMDEuanBnIiwiZWRpdHMiOnsiZ3JheXNjYWxlIjoidHJ1ZSJ9fQ==' + } + process.env = { + SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" + } + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.parseImageBucket(event, 'Default'); + // Assert + const expectedResult = 'allowedBucket001'; + assert.deepEqual(result, expectedResult); + }); + }); + describe('002/defaultRequestType/bucketSpecifiedInRequest/notAllowed', function() { + it(`Should throw an error if the bucket name is provided in the image request + but has not been whitelisted in SOURCE_BUCKETS`, function() { + // Arrange + const event = { + path : '/eyJidWNrZXQiOiJhbGxvd2VkQnVja2V0MDAxIiwia2V5Ijoic2FtcGxlSW1hZ2VLZXkwMDEuanBnIiwiZWRpdHMiOnsiZ3JheXNjYWxlIjoidHJ1ZSJ9fQ==' + } + process.env = { + SOURCE_BUCKETS : "allowedBucket003, allowedBucket004" + } + // Act + const imageRequest = new ImageRequest(); + // Assert + assert.throws(function() { + imageRequest.parseImageBucket(event, 'Default'); + }, Object, { + status: 403, + code: 'ImageBucket::CannotAccessBucket', + message: 'The bucket you specified could not be accessed. Please check that the bucket is specified in your SOURCE_BUCKETS.' + }); + }); + }); + describe('003/defaultRequestType/bucketNotSpecifiedInRequest', function() { + it(`Should pass if the image request does not contain a source bucket + but SOURCE_BUCKETS contains at least one bucket that can be + used as a default`, function() { + // Arrange + const event = { + path : '/eyJrZXkiOiJzYW1wbGVJbWFnZUtleTAwMS5qcGciLCJlZGl0cyI6eyJncmF5c2NhbGUiOiJ0cnVlIn19==' + } + process.env = { + SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" + } + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.parseImageBucket(event, 'Default'); + // Assert + const expectedResult = 'allowedBucket001'; + assert.deepEqual(result, expectedResult); + }); + }); + describe('004/thumborRequestType', function() { + it(`Should pass if there is at least one SOURCE_BUCKET specified that can + be used as the default for Thumbor requests`, function() { + // Arrange + const event = { + path : "/filters:grayscale()/test-image-001.jpg" + } + process.env = { + SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" + } + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.parseImageBucket(event, 'Thumbor'); + // Assert + const expectedResult = 'allowedBucket001'; + assert.deepEqual(result, expectedResult); + }); + }); + describe('005/customRequestType', function() { + it(`Should pass if there is at least one SOURCE_BUCKET specified that can + be used as the default for Custom requests`, function() { + // Arrange + const event = { + path : "/filters:grayscale()/test-image-001.jpg" + } + process.env = { + SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" + } + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.parseImageBucket(event, 'Custom'); + // Assert + const expectedResult = 'allowedBucket001'; + assert.deepEqual(result, expectedResult); + }); + }); + describe('006/invalidRequestType', function() { + it(`Should pass if there is at least one SOURCE_BUCKET specified that can + be used as the default for Custom requests`, function() { + // Arrange + const event = { + path : "/filters:grayscale()/test-image-001.jpg" + } + process.env = { + SOURCE_BUCKETS : "allowedBucket001, allowedBucket002" + } + // Act + const imageRequest = new ImageRequest(); + // Assert + assert.throws(function() { + imageRequest.parseImageBucket(event, undefined); + }, Object, { + status: 400, + code: 'ImageBucket::CannotFindBucket', + message: 'The bucket you specified could not be found. Please check the spelling of the bucket name in your request.' + }); + }); + }); +}); + +// ---------------------------------------------------------------------------- +// parseImageEdits() +// ---------------------------------------------------------------------------- +describe('parseImageEdits()', function() { + describe('001/defaultRequestType', function() { + it(`Should pass if the proper result is returned for a sample base64- + encoded image request`, function() { + // Arrange + const event = { + path : '/eyJlZGl0cyI6eyJncmF5c2NhbGUiOiJ0cnVlIiwicm90YXRlIjo5MCwiZmxpcCI6InRydWUifX0=' + } + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.parseImageEdits(event, 'Default'); + // Assert + const expectedResult = { + grayscale: 'true', + rotate: 90, + flip: 'true' + } + assert.deepEqual(result, expectedResult); + }); + }); + describe('002/thumborRequestType', function() { + it(`Should pass if the proper result is returned for a sample thumbor- + type image request`, function() { + // Arrange + const event = { + path : '/filters:rotate(90)/filters:grayscale()/thumbor-image.jpg' + } + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.parseImageEdits(event, 'Thumbor'); + // Assert + const expectedResult = { + rotate: 90, + grayscale: true + } + assert.deepEqual(result, expectedResult); + }); + }); + describe('003/customRequestType', function() { + it(`Should pass if the proper result is returned for a sample custom- + type image request`, function() { + // Arrange + const event = { + path : '/filters-rotate(90)/filters-grayscale()/thumbor-image.jpg' + } + process.env.REWRITE_MATCH_PATTERN = /(filters-)/gm; + process.env.REWRITE_SUBSTITUTION = 'filters:'; + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.parseImageEdits(event, 'Custom'); + // Assert + const expectedResult = { + rotate: 90, + grayscale: true + } + assert.deepEqual((typeof result !== undefined), !undefined) + }); + }); + describe('004/customRequestType', function() { + it(`Should throw an error if a requestType is not specified and/or the image edits + cannot be parsed`, function() { + // Arrange + const event = { + path : '/filters:rotate(90)/filters:grayscale()/other-image.jpg' + } + // Act + const imageRequest = new ImageRequest(); + // Assert + assert.throws(function() { + imageRequest.parseImageEdits(event, undefined); + }, Object, { + status: 400, + code: 'ImageEdits::CannotParseEdits', + message: 'The edits you provided could not be parsed. Please check the syntax of your request and refer to the documentation for additional guidance.' + }); + }); + }); +}); + +// ---------------------------------------------------------------------------- +// parseImageKey() +// ---------------------------------------------------------------------------- +describe('parseImageKey()', function() { + describe('001/defaultRequestType', function() { + it(`Should pass if an image key value is provided in the default + request format`, function() { + // Arrange + const event = { + path : '/eyJidWNrZXQiOiJteS1zYW1wbGUtYnVja2V0Iiwia2V5Ijoic2FtcGxlLWltYWdlLTAwMS5qcGcifQ==' + } + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.parseImageKey(event, 'Default'); + // Assert + const expectedResult = 'sample-image-001.jpg'; + assert.deepEqual(result, expectedResult); + }); + }); + describe('002/thumborRequestType', function() { + it(`Should pass if an image key value is provided in the thumbor + request format`, function() { + // Arrange + const event = { + path : '/filters:rotate(90)/filters:grayscale()/thumbor-image.jpg' + } + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.parseImageKey(event, 'Thumbor'); + // Assert + const expectedResult = 'thumbor-image.jpg'; + assert.deepEqual(result, expectedResult); + }); + }); + describe('003/customRequestType', function() { + it(`Should pass if an image key value is provided in the custom + request format`, function() { + // Arrange + const event = { + path : '/filters-rotate(90)/filters-grayscale()/custom-image.jpg' + } + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.parseImageKey(event, 'Custom'); + // Assert + const expectedResult = 'custom-image.jpg'; + assert.deepEqual(result, expectedResult); + }); + }); + describe('004/elseCondition', function() { + it(`Should throw an error if an unrecognized requestType is passed into the + function as a parameter`, function() { + // Arrange + const event = { + path : '/filters:rotate(90)/filters:grayscale()/other-image.jpg' + } + // Act + const imageRequest = new ImageRequest(); + // Assert + assert.throws(function() { + imageRequest.parseImageKey(event, undefined); + }, Object, { + status: 400, + code: 'ImageEdits::CannotFindImage', + message: 'The image you specified could not be found. Please check your request syntax as well as the bucket you specified to ensure it exists.' + }); + }); + }); +}); + +// ---------------------------------------------------------------------------- +// parseRequestType() +// ---------------------------------------------------------------------------- +describe('parseRequestType()', function() { + describe('001/defaultRequestType', function() { + it(`Should pass if the method detects a default request`, function() { + // Arrange + const event = { + path: '/eyJidWNrZXQiOiJteS1zYW1wbGUtYnVja2V0Iiwia2V5IjoibXktc2FtcGxlLWtleSIsImVkaXRzIjp7ImdyYXlzY2FsZSI6dHJ1ZX19' + } + process.env = {}; + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.parseRequestType(event); + // Assert + const expectedResult = 'Default'; + assert.deepEqual(result, expectedResult); + }); + }); + describe('002/thumborRequestType', function() { + it(`Should pass if the method detects a thumbor request`, function() { + // Arrange + const event = { + path: '/unsafe/filters:brightness(10):contrast(30)/https://upload.wikimedia.org/wikipedia/commons/thumb/7/79/Coffee_berries_1.jpg/1200px-Coffee_berries_1.jpg' + } + process.env = {}; + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.parseRequestType(event); + // Assert + const expectedResult = 'Thumbor'; + assert.deepEqual(result, expectedResult); + }); + }); + describe('003/customRequestType', function() { + it(`Should pass if the method detects a custom request`, function() { + // Arrange + const event = { + path: '/additionalImageRequestParameters/image.jpg' + } + process.env = { + REWRITE_MATCH_PATTERN: 'matchPattern', + REWRITE_SUBSTITUTION: 'substitutionString' + } + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.parseRequestType(event); + // Assert + const expectedResult = 'Custom'; + assert.deepEqual(result, expectedResult); + }); + }); + describe('004/elseCondition', function() { + it(`Should throw an error if the method cannot determine the request + type based on the three groups given`, function() { + // Arrange + const event = { + path : '12x12e24d234r2ewxsad123d34r' + } + process.env = {}; + // Act + const imageRequest = new ImageRequest(); + // Assert + assert.throws(function() { + const a = imageRequest.parseRequestType(event); + }, Object, { + status: 400, + code: 'RequestType::CannotDetermineRequestType', + message: 'The type of request you are making could not be properly routed. Please check your request syntax and refer to the documentation for additional guidance.' + }); + }); + }); +}); + +// ---------------------------------------------------------------------------- +// decodeRequest() +// ---------------------------------------------------------------------------- +describe('decodeRequest()', function() { + describe('001/validRequestPathSpecified', function() { + it(`Should pass if a valid base64-encoded path has been specified`, + function() { + // Arrange + const event = { + path : '/eyJidWNrZXQiOiJidWNrZXQtbmFtZS1oZXJlIiwia2V5Ijoia2V5LW5hbWUtaGVyZSJ9' + } + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.decodeRequest(event); + // Assert + const expectedResult = { + bucket: 'bucket-name-here', + key: 'key-name-here' + }; + assert.deepEqual(result, expectedResult); + }); + }); + describe('002/invalidRequestPathSpecified', function() { + it(`Should throw an error if a valid base64-encoded path has not been specified`, + function() { + // Arrange + const event = { + path : '/someNonBase64EncodedContentHere' + } + // Act + const imageRequest = new ImageRequest(); + // Assert + assert.throws(function() { + imageRequest.decodeRequest(event); + }, Object, { + status: 400, + code: 'DecodeRequest::CannotDecodeRequest', + message: 'The image request you provided could not be decoded. Please check that your request is base64 encoded properly and refer to the documentation for additional guidance.' + }); + }); + }); + describe('003/noPathSpecified', function() { + it(`Should throw an error if no path is specified at all`, + function() { + // Arrange + const event = {} + // Act + const imageRequest = new ImageRequest(); + // Assert + assert.throws(function() { + imageRequest.decodeRequest(event); + }, Object, { + status: 400, + code: 'DecodeRequest::CannotReadPath', + message: 'The URL path you provided could not be read. Please ensure that it is properly formed according to the solution documentation.' + }); + }); + }); +}); + +// ---------------------------------------------------------------------------- +// getAllowedSourceBuckets() +// ---------------------------------------------------------------------------- +describe('getAllowedSourceBuckets()', function() { + describe('001/sourceBucketsSpecified', function() { + it(`Should pass if the SOURCE_BUCKETS environment variable is not empty + and contains valid inputs`, function() { + // Arrange + process.env = { + SOURCE_BUCKETS: 'allowedBucket001, allowedBucket002' + } + // Act + const imageRequest = new ImageRequest(); + const result = imageRequest.getAllowedSourceBuckets(); + // Assert + const expectedResult = ['allowedBucket001', 'allowedBucket002']; + assert.deepEqual(result, expectedResult); + }); + }); + describe('002/noSourceBucketsSpecified', function() { + it(`Should throw an error if the SOURCE_BUCKETS environment variable is + empty or does not contain valid values`, function() { + // Arrange + process.env = {}; + // Act + const imageRequest = new ImageRequest(); + // Assert + assert.throws(function() { + imageRequest.getAllowedSourceBuckets(); + }, Object, { + status: 400, + code: 'GetAllowedSourceBuckets::NoSourceBuckets', + message: 'The SOURCE_BUCKETS variable could not be read. Please check that it is not empty and contains at least one source bucket, or multiple buckets separated by commas. Spaces can be provided between commas and bucket names, these will be automatically parsed out when decoding.' + }); + }); + }); +}); + +// ---------------------------------------------------------------------------- +// getOutputFormat() +// ---------------------------------------------------------------------------- +describe('getOutputFormat()', function () { + describe('001/AcceptsHeaderIncludesWebP', function () { + it(`Should pass if it returns "webp" for an accepts header which includes webp`, function () { + // Arrange + process.env = { + AUTO_WEBP: true + }; + const event = { + headers: { + Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3" + } + }; + // Act + const imageRequest = new ImageRequest(); + var result = imageRequest.getOutputFormat(event); + // Assert + assert.deepEqual(result, 'webp'); + }); + }); + describe('002/AcceptsHeaderDoesNotIncludeWebP', function () { + it(`Should pass if it returns null for an accepts header which does not include webp`, function () { + // Arrange + process.env = { + AUTO_WEBP: true + }; + const event = { + headers: { + Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/apng,*/*;q=0.8,application/signed-exchange;v=b3" + } + }; + // Act + const imageRequest = new ImageRequest(); + var result = imageRequest.getOutputFormat(event); + // Assert + assert.deepEqual(result, null); + }); + }); + describe('003/AutoWebPDisabled', function () { + it(`Should pass if it returns null when AUTO_WEBP is disabled with accepts header including webp`, function () { + // Arrange + process.env = { + AUTO_WEBP: false + }; + const event = { + headers: { + Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3" + } + }; + // Act + const imageRequest = new ImageRequest(); + var result = imageRequest.getOutputFormat(event); + // Assert + assert.deepEqual(result, null); + }); + }); + describe('004/AutoWebPUnset', function () { + it(`Should pass if it returns null when AUTO_WEBP is not set with accepts header including webp`, function () { + // Arrange + const event = { + headers: { + Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3" + } + }; + // Act + const imageRequest = new ImageRequest(); + var result = imageRequest.getOutputFormat(event); + // Assert + assert.deepEqual(result, null); + }); + }); +}); diff --git a/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/test/test-thumbor-mapping.js b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/test/test-thumbor-mapping.js new file mode 100755 index 000000000..1186bc5d3 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/test/test-thumbor-mapping.js @@ -0,0 +1,842 @@ +/********************************************************************************************************************* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * + * with the License. A copy of the License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * + * and limitations under the License. * + *********************************************************************************************************************/ + +const ThumborMapping = require('../thumbor-mapping'); +let assert = require('assert'); + +// ---------------------------------------------------------------------------- +// process() +// ---------------------------------------------------------------------------- +describe('process()', function() { + describe('001/thumborRequest', function() { + it(`Should pass if the proper edit translations are applied and in the + correct order`, function() { + // Arrange + const event = { + path : "/fit-in/200x300/filters:grayscale()/test-image-001.jpg" + } + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.process(event); + // Assert + const expectedResult = { + edits: { + resize: { + width: 200, + height: 300, + fit: 'inside' + }, + grayscale: true + } + }; + assert.deepEqual(thumborMapping.edits, expectedResult.edits); + }); + }); +}); + +// ---------------------------------------------------------------------------- +// parseCustomPath() +// ---------------------------------------------------------------------------- +describe('parseCustomPath()', function() { + describe('001/validPath', function() { + it(`Should pass if the proper edit translations are applied and in the + correct order`, function() { + const event = { + path : '/filters-rotate(90)/filters-grayscale()/thumbor-image.jpg' + } + process.env.REWRITE_MATCH_PATTERN = /(filters-)/gm; + process.env.REWRITE_SUBSTITUTION = 'filters:'; + // Act + const thumborMapping = new ThumborMapping(); + const result = thumborMapping.parseCustomPath(event.path); + // Assert + const expectedResult = '/filters:rotate(90)/filters:grayscale()/thumbor-image.jpg'; + assert.deepEqual(result.path, expectedResult); + }); + }); + describe('002/undefinedEnvironmentVariables', function() { + it(`Should throw an error if the environment variables are left undefined`, function() { + const event = { + path : '/filters-rotate(90)/filters-grayscale()/thumbor-image.jpg' + } + process.env.REWRITE_MATCH_PATTERN = undefined; + process.env.REWRITE_SUBSTITUTION = undefined; + // Act + const thumborMapping = new ThumborMapping(); + // Assert + assert.throws(function() { + thumborMapping.parseCustomPath(event.path); + }, Error, 'ThumborMapping::ParseCustomPath::ParsingError'); + }); + }); + describe('003/undefinedPath', function() { + it(`Should throw an error if the path is not defined`, function() { + const event = {}; + process.env.REWRITE_MATCH_PATTERN = /(filters-)/gm; + process.env.REWRITE_SUBSTITUTION = 'filters:'; + // Act + const thumborMapping = new ThumborMapping(); + // Assert + assert.throws(function() { + thumborMapping.parseCustomPath(event.path); + }, Error, 'ThumborMapping::ParseCustomPath::ParsingError'); + }); + }); + describe('004/undefinedAll', function() { + it(`Should throw an error if the path is not defined`, function() { + const event = {}; + process.env.REWRITE_MATCH_PATTERN = undefined; + process.env.REWRITE_SUBSTITUTION = undefined; + // Act + const thumborMapping = new ThumborMapping(); + // Assert + assert.throws(function() { + thumborMapping.parseCustomPath(event.path); + }, Error, 'ThumborMapping::ParseCustomPath::ParsingError'); + }); + }); +}); + +// ---------------------------------------------------------------------------- +// mapFilter() +// ---------------------------------------------------------------------------- +describe('mapFilter()', function() { + describe('001/autojpg', function() { + it(`Should pass if the filter is successfully converted from + Thumbor:autojpg()`, function() { + // Arrange + const edit = 'filters:autojpg()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { toFormat: 'jpeg' } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('002/background_color', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:background_color()`, function() { + // Arrange + const edit = 'filters:background_color(ffff)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { flatten: { background: {r: 255, g: 255, b: 255}}} + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('003/blur/singleParameter', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:blur()`, function() { + // Arrange + const edit = 'filters:blur(60)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { blur: 30 } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('004/blur/doubleParameter', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:blur()`, function() { + // Arrange + const edit = 'filters:blur(60, 2)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { blur: 2 } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('005/convolution', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:convolution()`, function() { + // Arrange + const edit = 'filters:convolution(1;2;1;2;4;2;1;2;1,3,true)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { convolve: { + width: 3, + height: 3, + kernel: [1,2,1,2,4,2,1,2,1] + }} + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('006/equalize', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:equalize()`, function() { + // Arrange + const edit = 'filters:equalize()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { normalize: 'true' } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('007/fill/resizeUndefined', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:fill()`, function() { + // Arrange + const edit = 'filters:fill(fff)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { resize: { background: { r: 255, g: 255, b: 255 } }} + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + + describe('008/fill/resizeDefined', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:fill()`, function() { + // Arrange + const edit = 'filters:fill(fff)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.edits.resize = {}; + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { resize: { background: { r: 255, g: 255, b: 255 } }} + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('009/format/supportedFileType', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:format()`, function() { + // Arrange + const edit = 'filters:format(png)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { toFormat: 'png' } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('010/format/unsupportedFileType', function() { + it(`Should return undefined if an accepted file format is not specified` + , function() { + // Arrange + const edit = 'filters:format(test)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('011/no_upscale/resizeUndefined', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:no_upscale()`, function() { + // Arrange + const edit = 'filters:no_upscale()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + resize: { + withoutEnlargement: true + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('012/no_upscale/resizeDefined', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:no_upscale()`, function() { + // Arrange + const edit = 'filters:no_upscale()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.edits.resize = { + height: 400, + width: 300 + }; + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + resize: { + height: 400, + width: 300, + withoutEnlargement: true + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('013/proportion/resizeDefined', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:proportion()`, function() { + // Arrange + const edit = 'filters:proportion(0.3)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.edits = { + resize: { + width: 200, + height: 200 + } + }; + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + resize: { + height: 60, + width: 60 + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('014/proportion/resizeUndefined', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:resize()`, function() { + // Arrange + const edit = 'filters:proportion(0.3)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const actualResult = (typeof(thumborMapping.edits.resize) !== undefined); + const expectedResult = true; + assert.deepEqual(actualResult, expectedResult); + }); + }); + describe('015/quality/jpg', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:quality()`, function() { + // Arrange + const edit = 'filters:quality(50)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + jpeg: { + quality: 50 + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('016/quality/png', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:quality()`, function() { + // Arrange + const edit = 'filters:quality(50)'; + const filetype = 'png'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + png: { + quality: 50 + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('017/quality/webp', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:quality()`, function() { + // Arrange + const edit = 'filters:quality(50)'; + const filetype = 'webp'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + webp: { + quality: 50 + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('018/quality/tiff', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:quality()`, function() { + // Arrange + const edit = 'filters:quality(50)'; + const filetype = 'tiff'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + tiff: { + quality: 50 + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('019/quality/heif', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:quality()`, function() { + // Arrange + const edit = 'filters:quality(50)'; + const filetype = 'heif'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + heif: { + quality: 50 + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('020/quality/other', function() { + it(`Should return undefined if an unsupported file type is provided`, + function() { + // Arrange + const edit = 'filters:quality(50)'; + const filetype = 'xml'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('021/rgb', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:rgb()`, function() { + // Arrange + const edit = 'filters:rgb(10, 10, 10)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + tint: { + r: 25.5, + g: 25.5, + b: 25.5 + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('022/rotate', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:rotate()`, function() { + // Arrange + const edit = 'filters:rotate(75)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + rotate: 75 + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('023/sharpen', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:sharpen()`, function() { + // Arrange + const edit = 'filters:sharpen(75, 5)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + sharpen: 3.5 + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('024/stretch/default', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:stretch()`, function() { + // Arrange + const edit = 'filters:stretch()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + resize: { fit: 'fill' } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('025/stretch/resizeDefined', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:stretch()`, function() { + // Arrange + const edit = 'filters:stretch()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.edits.resize = {}; + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + resize: { fit: 'fill' } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('026/stretch/sizingMethodUndefined', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:stretch()`, function() { + // Arrange + const edit = 'filters:stretch()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.edits.resize = {}; + thumborMapping.sizingMethod = undefined; + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + resize: { fit: 'fill' } + }, + sizingMethod: undefined + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('027/stretch/sizingMethodNotFitIn', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:stretch()`, function() { + // Arrange + const edit = 'filters:stretch()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.edits.resize = {}; + thumborMapping.sizingMethod = "cover"; + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + resize: { fit: 'fill' } + }, + sizingMethod: "cover" + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('028/stretch/sizingMethodFitIn', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:stretch()`, function() { + // Arrange + const edit = 'filters:stretch()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.edits.resize = {}; + thumborMapping.sizingMethod = "fit-in"; + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + resize: {} + }, + sizingMethod: "fit-in" + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('029/strip_exif', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:strip_exif()`, function() { + // Arrange + const edit = 'filters:strip_exif()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + rotate: 0 + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('030/strip_icc', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:strip_icc()`, function() { + // Arrange + const edit = 'filters:strip_icc()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + rotate: 0 + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('031/upscale', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:upscale()`, function() { + // Arrange + const edit = 'filters:upscale()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + resize: { + fit: 'inside' + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('032/upscale/resizeNotUndefined', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:upscale()`, function() { + // Arrange + const edit = 'filters:upscale()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.edits.resize = {}; + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + resize: { + fit: 'inside' + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('032/watermark/positionDefined', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:watermark()`, function() { + // Arrange + const edit = 'filters:watermark(bucket,key,100,100,0)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + overlayWith: { + bucket: 'bucket', + key: 'key', + alpha: '0', + wRatio: undefined, + hRatio: undefined, + options: { + left: '100', + top: '100' + } + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('033/watermark/positionDefinedByPercentile', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:watermark()`, function() { + // Arrange + const edit = 'filters:watermark(bucket,key,50p,30p,0)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + overlayWith: { + bucket: 'bucket', + key: 'key', + alpha: '0', + wRatio: undefined, + hRatio: undefined, + options: { + left: '50p', + top: '30p' + } + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('034/watermark/positionDefinedWrong', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:watermark()`, function() { + // Arrange + const edit = 'filters:watermark(bucket,key,x,x,0)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + overlayWith: { + bucket: 'bucket', + key: 'key', + alpha: '0', + wRatio: undefined, + hRatio: undefined, + options: {} + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('035/watermark/ratioDefined', function() { + it(`Should pass if the filter is successfully translated from + Thumbor:watermark()`, function() { + // Arrange + const edit = 'filters:watermark(bucket,key,100,100,0,10,10)'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { + edits: { + overlayWith: { + bucket: 'bucket', + key: 'key', + alpha: '0', + wRatio: '10', + hRatio: '10', + options: { + left: '100', + top: '100' + } + } + } + }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); + describe('036/elseCondition', function() { + it(`Should pass if undefined is returned for an unsupported filter`, + function() { + // Arrange + const edit = 'filters:notSupportedFilter()'; + const filetype = 'jpg'; + // Act + const thumborMapping = new ThumborMapping(); + thumborMapping.mapFilter(edit, filetype); + // Assert + const expectedResult = { edits: {} }; + assert.deepEqual(thumborMapping, expectedResult); + }); + }); +}) \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/thumbor-mapping.js b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/thumbor-mapping.js new file mode 100755 index 000000000..4dca04f57 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/lib/lambda/image-handler/thumbor-mapping.js @@ -0,0 +1,256 @@ +/********************************************************************************************************************* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * + * with the License. A copy of the License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * + * and limitations under the License. * + *********************************************************************************************************************/ + +const Color = require('color'); +const ColorName = require('color-name'); + +class ThumborMapping { + + // Constructor + constructor() { + this.edits = {}; + this.sizingMethod; + } + + /** + * Initializer function for creating a new Thumbor mapping, used by the image + * handler to perform image modifications based on legacy URL path requests. + * @param {Object} event - The request body. + */ + process(event) { + // Setup + this.path = event.path; + const edits = this.path.split('/'); + const filetype = (this.path.split('.'))[(this.path.split('.')).length - 1]; + + // Process the Dimensions + const dimPath = this.path.match(/[^\/]\d+x\d+/g); + if (dimPath) { + const dims = dimPath[0].split('x'); + // Set only if the dimensions provided are valid + if (!isNaN(dims[0]) && !isNaN(dims[1])) { + this.edits.resize = {}; + this.edits.resize.fit = 'fill'; + + // Assign dimenions from the first match only to avoid parsing dimension from image file names + this.edits.resize.width = Number(dims[0]); + this.edits.resize.height = Number(dims[1]); + } + } + + // Parse the image path + for (let i = 0; i < edits.length; i++) { + const edit = edits[i]; + if (edit === ('fit-in')) { + if (this.edits.resize === undefined) { + this.edits.resize = {}; + } + + this.edits.resize.fit = 'inside'; + this.sizingMethod = edit; + } else if (edit.includes('filters:')) { + this.mapFilter(edit, filetype); + } + } + + return this; + } + + /** + * Enables users to migrate their current image request model to the SIH solution, + * without changing their legacy application code to accomodate new image requests. + * @param {String} path - The URL path extracted from the web request. + */ + parseCustomPath(path) { + // Setup from the environment variables + const matchPattern = process.env.REWRITE_MATCH_PATTERN; + const substitution = process.env.REWRITE_SUBSTITUTION; + // Perform the substitution and return + if (path !== undefined && matchPattern !== undefined && substitution !== undefined) { + const parsedPath = path.replace(matchPattern, substitution); + const output = { path: parsedPath }; + return output; + } else { + throw new Error('ThumborMapping::ParseCustomPath::ParsingError'); + } + } + + /** + * Scanner function for matching supported Thumbor filters and converting their + * capabilities into Sharp.js supported operations. + * @param {String} edit - The URL path filter. + * @param {String} filetype - The file type of the original image. + */ + mapFilter(edit, filetype) { + const matched = edit.match(/:(.+)\((.*)\)/); + const key = matched[1]; + let value = matched[2]; + // Find the proper filter + if (key === ('autojpg')) { + this.edits.toFormat = 'jpeg'; + } + else if (key === ('background_color')) { + if (!ColorName[value]) { + value = `#${value}` + } + this.edits.flatten = { background: Color(value).object() }; + } + else if (key === ('blur')) { + const val = value.split(','); + this.edits.blur = (val.length > 1) ? Number(val[1]) : Number(val[0]) / 2; + } + else if (key === ('convolution')) { + const arr = value.split(','); + const strMatrix = (arr[0]).split(';'); + let matrix = []; + strMatrix.forEach(function(str) { + matrix.push(Number(str)); + }); + const matrixWidth = arr[1]; + let matrixHeight = 0; + let counter = 0; + for (let i = 0; i < matrix.length; i++) { + if (counter === (matrixWidth - 1)) { + matrixHeight++; + counter = 0; + } else { + counter++; + } + } + this.edits.convolve = { + width: Number(matrixWidth), + height: Number(matrixHeight), + kernel: matrix + } + } + else if (key === ('equalize')) { + this.edits.normalize = "true"; + } + else if (key === ('fill')) { + if (this.edits.resize === undefined) { + this.edits.resize = {}; + } + if (!ColorName[value]) { + value = `#${value}` + } + this.edits.resize.background = Color(value).object(); + } + else if (key === ('format')) { + const formattedValue = value.replace(/[^0-9a-z]/gi, '').replace(/jpg/i, 'jpeg'); + const acceptedValues = ['heic', 'heif', 'jpeg', 'png', 'raw', 'tiff', 'webp']; + if (acceptedValues.includes(formattedValue)) { + this.edits.toFormat = formattedValue; + } + } + else if (key === ('grayscale')) { + this.edits.grayscale = true; + } + else if (key === ('no_upscale')) { + if (this.edits.resize === undefined) { + this.edits.resize = {}; + } + this.edits.resize.withoutEnlargement = true; + } + else if (key === ('proportion')) { + if (this.edits.resize === undefined) { + this.edits.resize = {}; + } + const prop = Number(value); + this.edits.resize.width = Number(this.edits.resize.width * prop); + this.edits.resize.height = Number(this.edits.resize.height * prop); + } + else if (key === ('quality')) { + if (['jpg', 'jpeg'].includes(filetype)) { + this.edits.jpeg = { quality: Number(value) } + } else if (filetype === 'png') { + this.edits.png = { quality: Number(value) } + } else if (filetype === 'webp') { + this.edits.webp = { quality: Number(value) } + } else if (filetype === 'tiff') { + this.edits.tiff = { quality: Number(value) } + } else if (filetype === 'heif') { + this.edits.heif = { quality: Number(value) } + } + } + else if (key === ('rgb')) { + const percentages = value.split(','); + const values = []; + percentages.forEach(function (percentage) { + const parsedPercentage = Number(percentage); + const val = 255 * (parsedPercentage / 100); + values.push(val); + }) + this.edits.tint = { r: values[0], g: values[1], b: values[2] }; + } + else if (key === ('rotate')) { + this.edits.rotate = Number(value); + } + else if (key === ('sharpen')) { + const sh = value.split(','); + const sigma = 1 + Number(sh[1]) / 2; + this.edits.sharpen = sigma; + } + else if (key === ('stretch')) { + if (this.edits.resize === undefined) { + this.edits.resize = {}; + } + if (this.sizingMethod === undefined || this.sizingMethod !== 'fit-in') { + this.edits.resize.fit = "fill"; + } + } + else if (key === ('strip_exif')) { + this.edits.rotate = 0; + } + else if (key === ('strip_icc')) { + this.edits.rotate = 0; + } + else if (key === ('upscale')) { + if (this.edits.resize === undefined) { + this.edits.resize = {}; + } + this.edits.resize.fit = "inside" + } + else if (key === ('watermark')) { + const options = value.replace(/\s+/g, '').split(','); + const bucket = options[0]; + const key = options[1]; + const xPos = options[2]; + const yPos = options[3]; + const alpha = options[4]; + const wRatio = options[5]; + const hRatio = options[6]; + + this.edits.overlayWith = { + bucket, + key, + alpha, + wRatio, + hRatio, + options: {} + } + const allowedPosPattern = /^(100|[1-9]?[0-9]|-(100|[1-9][0-9]?))p$/; + if (allowedPosPattern.test(xPos) || !isNaN(xPos)) { + this.edits.overlayWith.options['left'] = xPos; + } + if (allowedPosPattern.test(yPos) || !isNaN(yPos)) { + this.edits.overlayWith.options['top'] = yPos; + } + } + else { + return undefined; + } + } +} + +// Exports +module.exports = ThumborMapping; \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/package.json b/source/use_cases/aws-serverless-image-handler/package.json new file mode 100644 index 000000000..76fbb415f --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/package.json @@ -0,0 +1,87 @@ +{ + "name": "@aws-konstruk/aws-serverless-image-handler", + "version": "0.8.0", + "description": "Use case pattern for deploying a serverless image handler API.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-konstruk.git", + "directory": "source/patterns/@aws-konstruk/aws-serverless-image-handler" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.konstruk.services.serverlessimagehandler", + "maven": { + "groupId": "software.amazon.konstruk", + "artifactId": "serverlessimagehandler" + } + }, + "dotnet": { + "namespace": "Amazon.Konstruk.AWS.ServerlessImageHandler", + "packageId": "Amazon.Konstruk.AWS.ServerlessImageHandler", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-konstruk.aws-serverless-image-handler", + "module": "aws_konstruk.aws_serverless_image_handler" + } + } + }, + "dependencies": { + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-cdk/aws-cloudfront": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-cdk/core": "~1.25.0", + "@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda": "~0.8.0", + "@aws-solutions-konstruk/aws-lambda-s3": "~0.8.0", + "@aws-solutions-konstruk/core": "~0.8.0" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + }, + "peerDependencies": { + "@aws-solutions-konstruk/aws-cloudfront-apigateway-lambda": "~0.8.0", + "@aws-solutions-konstruk/aws-lambda-s3": "~0.8.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-cdk/aws-cloudfront": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "@aws-cdk/aws-iam": "~1.25.0" + } +} diff --git a/source/use_cases/aws-serverless-image-handler/test/__snapshots__/test.serverless-image-handler.test.js.snap b/source/use_cases/aws-serverless-image-handler/test/__snapshots__/test.serverless-image-handler.test.js.snap new file mode 100644 index 000000000..b4d56b094 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/test/__snapshots__/test.serverless-image-handler.test.js.snap @@ -0,0 +1,910 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Minimal deployment snapshot test 1`] = ` +Object { + "Outputs": Object { + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiEndpoint76827D71": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentStageprodBE3E04B8", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96ArtifactHash7AE27721": Object { + "Description": "Artifact hash for asset \\"5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96\\"", + "Type": "String", + }, + "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3Bucket65CDB50E": Object { + "Description": "S3 bucket for asset \\"5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96\\"", + "Type": "String", + }, + "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3VersionKeyCF89D2F1": Object { + "Description": "S3 key for asset version \\"5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96\\"", + "Type": "String", + }, + }, + "Resources": Object { + "testserverlessimagehandlerCloudFrontApiGatewayLambdaApiAccessLogGroup75A8AB40": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudFrontDistributionCFDistribution5DCC756A": Object { + "Properties": Object { + "DistributionConfig": Object { + "DefaultCacheBehavior": Object { + "AllowedMethods": Array [ + "GET", + "HEAD", + ], + "CachedMethods": Array [ + "GET", + "HEAD", + ], + "Compress": true, + "ForwardedValues": Object { + "Cookies": Object { + "Forward": "none", + }, + "QueryString": false, + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https", + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": Object { + "Bucket": Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudfrontLoggingBucket58AA7378", + "RegionalDomainName", + ], + }, + "IncludeCookies": false, + }, + "Origins": Array [ + Object { + "CustomOriginConfig": Object { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginKeepaliveTimeout": 5, + "OriginProtocolPolicy": "https-only", + "OriginReadTimeout": 30, + "OriginSSLProtocols": Array [ + "TLSv1.2", + ], + }, + "DomainName": Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "/", + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "://", + Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentStageprodBE3E04B8", + }, + "/", + ], + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + "Id": "origin1", + }, + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": Object { + "CloudFrontDefaultCertificate": true, + }, + }, + }, + "Type": "AWS::CloudFront::Distribution", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudfrontLoggingBucket58AA7378": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + Object { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiAccount372B2E2D": Object { + "DependsOn": Array [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiCloudWatchRole21DC3987", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiCloudWatchRole21DC3987": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC": Object { + "Properties": Object { + "BinaryMediaTypes": Array [ + "*/*", + ], + "EndpointConfiguration": Object { + "Types": Array [ + "REGIONAL", + ], + }, + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiANYApiPermissionTesttestserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi3B1AFDB4ANY1BF514F4": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerLambdaFunction78B3105C", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + }, + "/test-invoke-stage/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiANYApiPermissiontestserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi3B1AFDB4ANYA88E54B3": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerLambdaFunction78B3105C", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + }, + "/", + Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentStageprodBE3E04B8", + }, + "/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiANYC648EF96": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerLambdaFunction78B3105C", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentD7B20DCAf6e8220e21bde23f88e7afaa751339fe": Object { + "DependsOn": Array [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxyANY9BF7CFD0", + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxy5FD5FDA4", + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiANYC648EF96", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentStageprodBE3E04B8": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaApiAccessLogGroup75A8AB40", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentD7B20DCAf6e8220e21bde23f88e7afaa751339fe", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiUsagePlan6B0FADA4": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + }, + "Stage": Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentStageprodBE3E04B8", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxy5FD5FDA4": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + "RootResourceId", + ], + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxyANY9BF7CFD0": Object { + "Properties": Object { + "AuthorizationType": "AWS_IAM", + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerLambdaFunction78B3105C", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxy5FD5FDA4", + }, + "RestApiId": Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxyANYApiPermissionTesttestserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi3B1AFDB4ANYproxyC1D1533D": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerLambdaFunction78B3105C", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + }, + "/test-invoke-stage/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxyANYApiPermissiontestserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi3B1AFDB4ANYproxy2861CE67": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerLambdaFunction78B3105C", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + }, + "/", + Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentStageprodBE3E04B8", + }, + "/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "testserverlessimagehandlerExistingLambdaS3S3LoggingBucket406E2181", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "testserverlessimagehandlerExistingLambdaS3S3LoggingBucket406E2181": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "testserverlessimagehandlerLambdaFunction78B3105C": Object { + "DependsOn": Array [ + "testserverlessimagehandlerLambdaFunctionServiceRoleDefaultPolicyD1EA3899", + "testserverlessimagehandlerLambdaFunctionServiceRole744A1CF6", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3Bucket65CDB50E", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3VersionKeyCF89D2F1", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3VersionKeyCF89D2F1", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AUTO_WEBP": "No", + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "CORS_ENABLED": "Yes", + "CORS_ORIGIN": "*", + "S3_BUCKET_NAME": Object { + "Ref": "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662", + }, + "SOURCE_BUCKETS": Object { + "Fn::Join": Array [ + "", + Array [ + "my-sample-bucket,", + Object { + "Ref": "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662", + }, + ], + ], + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerLambdaFunctionServiceRole744A1CF6", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + }, + "Type": "AWS::Lambda::Function", + }, + "testserverlessimagehandlerLambdaFunctionServiceRole744A1CF6": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "testserverlessimagehandlerLambdaFunctionServiceRoleDefaultPolicyD1EA3899": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "testserverlessimagehandlerLambdaFunctionServiceRoleDefaultPolicyD1EA3899", + "Roles": Array [ + Object { + "Ref": "testserverlessimagehandlerLambdaFunctionServiceRole744A1CF6", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "testserverlessimagehandlerLambdaS3AccessPolicyD6DC56B2": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Specified Rekognition action needs wildcard resource.", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": "arn:aws:s3:::*", + }, + Object { + "Action": "rekognition:DetectFaces", + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "testserverlessimagehandlerLambdaS3AccessPolicyD6DC56B2", + "Roles": Array [ + Object { + "Ref": "testserverlessimagehandlerLambdaFunctionServiceRole744A1CF6", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + }, +} +`; diff --git a/source/use_cases/aws-serverless-image-handler/test/integ.basic-deployment.expected.json b/source/use_cases/aws-serverless-image-handler/test/integ.basic-deployment.expected.json new file mode 100644 index 000000000..fcdaeb69f --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/test/integ.basic-deployment.expected.json @@ -0,0 +1,907 @@ +{ + "Description": "Integration Test for aws-serverless-image-handler", + "Resources": { + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "BinaryMediaTypes": [ + "*/*" + ], + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Name": "RestApi" + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentD7B20DCAf6e8220e21bde23f88e7afaa751339fe": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxyANY9BF7CFD0", + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxy5FD5FDA4", + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiANYC648EF96" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentStageprodBE3E04B8": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaApiAccessLogGroup75A8AB40", + "Arn" + ] + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + }, + "DeploymentId": { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentD7B20DCAf6e8220e21bde23f88e7afaa751339fe" + }, + "MethodSettings": [ + { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod" + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxy5FD5FDA4": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + "RootResourceId" + ] + }, + "PathPart": "{proxy+}", + "RestApiId": { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC" + } + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxyANYApiPermissiontestserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiAA086636ANYproxy770FE38C": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "testserverlessimagehandlerLambdaFunction78B3105C", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC" + }, + "/", + { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentStageprodBE3E04B8" + }, + "/*/{proxy+}" + ] + ] + } + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxyANYApiPermissionTesttestserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiAA086636ANYproxy5FAC5F91": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "testserverlessimagehandlerLambdaFunction78B3105C", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC" + }, + "/test-invoke-stage/*/{proxy+}" + ] + ] + } + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxyANY9BF7CFD0": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiproxy5FD5FDA4" + }, + "RestApiId": { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "testserverlessimagehandlerLambdaFunction78B3105C", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiANYApiPermissiontestserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiAA086636ANY706A7345": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "testserverlessimagehandlerLambdaFunction78B3105C", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC" + }, + "/", + { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentStageprodBE3E04B8" + }, + "/*/" + ] + ] + } + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiANYApiPermissionTesttestserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiAA086636ANYBCCBE67D": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "testserverlessimagehandlerLambdaFunction78B3105C", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC" + }, + "/test-invoke-stage/*/" + ] + ] + } + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiANYC648EF96": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC" + }, + "AuthorizationType": "AWS_IAM", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "testserverlessimagehandlerLambdaFunction78B3105C", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiUsagePlan6B0FADA4": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC" + }, + "Stage": { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentStageprodBE3E04B8" + }, + "Throttle": {} + } + ] + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaApiAccessLogGroup75A8AB40": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiCloudWatchRole21DC3987": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiAccount372B2E2D": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiCloudWatchRole21DC3987", + "Arn" + ] + } + }, + "DependsOn": [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC" + ] + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudfrontLoggingBucket58AA7378": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + }, + { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + } + ] + } + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudFrontDistributionCFDistribution5DCC756A": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "AllowedMethods": [ + "GET", + "HEAD" + ], + "CachedMethods": [ + "GET", + "HEAD" + ], + "Compress": true, + "ForwardedValues": { + "Cookies": { + "Forward": "none" + }, + "QueryString": false + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https" + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": { + "Bucket": { + "Fn::GetAtt": [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudfrontLoggingBucket58AA7378", + "RegionalDomainName" + ] + }, + "IncludeCookies": false + }, + "Origins": [ + { + "CustomOriginConfig": { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginKeepaliveTimeout": 5, + "OriginProtocolPolicy": "https-only", + "OriginReadTimeout": 30, + "OriginSSLProtocols": [ + "TLSv1.2" + ] + }, + "DomainName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "://", + { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentStageprodBE3E04B8" + }, + "/" + ] + ] + } + ] + } + ] + } + ] + } + ] + }, + "Id": "origin1" + } + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": { + "CloudFrontDefaultCertificate": true + } + } + } + }, + "testserverlessimagehandlerLambdaFunctionServiceRole744A1CF6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "testserverlessimagehandlerLambdaFunctionServiceRoleDefaultPolicyD1EA3899": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testserverlessimagehandlerLambdaFunctionServiceRoleDefaultPolicyD1EA3899", + "Roles": [ + { + "Ref": "testserverlessimagehandlerLambdaFunctionServiceRole744A1CF6" + } + ] + } + }, + "testserverlessimagehandlerLambdaFunction78B3105C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3Bucket65CDB50E" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3VersionKeyCF89D2F1" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3VersionKeyCF89D2F1" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "testserverlessimagehandlerLambdaFunctionServiceRole744A1CF6", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "AUTO_WEBP": "No", + "CORS_ENABLED": "Yes", + "CORS_ORIGIN": "*", + "S3_BUCKET_NAME": { + "Ref": "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662" + }, + "SOURCE_BUCKETS": { + "Fn::Join": [ + "", + [ + "my-sample-bucket,", + { + "Ref": "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662" + } + ] + ] + } + } + } + }, + "DependsOn": [ + "testserverlessimagehandlerLambdaFunctionServiceRoleDefaultPolicyD1EA3899", + "testserverlessimagehandlerLambdaFunctionServiceRole744A1CF6" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "testserverlessimagehandlerExistingLambdaS3S3LoggingBucket406E2181": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + }, + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "testserverlessimagehandlerExistingLambdaS3S3LoggingBucket406E2181" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "testserverlessimagehandlerLambdaS3AccessPolicyD6DC56B2": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": "arn:aws:s3:::*" + }, + { + "Action": "rekognition:DetectFaces", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testserverlessimagehandlerLambdaS3AccessPolicyD6DC56B2", + "Roles": [ + { + "Ref": "testserverlessimagehandlerLambdaFunctionServiceRole744A1CF6" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Specified Rekognition action needs wildcard resource." + } + ] + } + } + } + }, + "Outputs": { + "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiEndpoint76827D71": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApi2376E6FC" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaRestApiDeploymentStageprodBE3E04B8" + }, + "/" + ] + ] + } + } + }, + "Parameters": { + "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3Bucket65CDB50E": { + "Type": "String", + "Description": "S3 bucket for asset \"5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96\"" + }, + "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3VersionKeyCF89D2F1": { + "Type": "String", + "Description": "S3 key for asset version \"5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96\"" + }, + "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96ArtifactHash7AE27721": { + "Type": "String", + "Description": "Artifact hash for asset \"5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96\"" + } + } +} \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/test/integ.basic-deployment.ts b/source/use_cases/aws-serverless-image-handler/test/integ.basic-deployment.ts new file mode 100644 index 000000000..fecc86751 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/test/integ.basic-deployment.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { ServerlessImageHandler, ServerlessImageHandlerProps } from "../lib"; + +// Setup +const app = new App(); +const stack = new Stack(app, 'test-serverless-image-handler'); +stack.templateOptions.description = 'Integration Test for aws-serverless-image-handler'; + +// Definitions +const props: ServerlessImageHandlerProps = { + corsEnabled: true, + corsOrigin: "*", + sourceBuckets: "my-sample-bucket", + logRetentionPeriod: 7 +}; + +new ServerlessImageHandler(stack, 'test-serverless-image-handler', props); + +// Synth +app.synth(); diff --git a/source/use_cases/aws-serverless-image-handler/test/test.serverless-image-handler.test.ts b/source/use_cases/aws-serverless-image-handler/test/test.serverless-image-handler.test.ts new file mode 100644 index 000000000..d8b1b2a13 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/test/test.serverless-image-handler.test.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// Imports +import { Stack } from "@aws-cdk/core"; +import { ServerlessImageHandler, ServerlessImageHandlerProps } from "../lib"; +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +// -------------------------------------------------------------- +// Minimal deployment snapshot test +// -------------------------------------------------------------- +test('Minimal deployment snapshot test', () => { + // Initial Setup + const stack = new Stack(); + const props: ServerlessImageHandlerProps = { + corsEnabled: true, + corsOrigin: "*", + sourceBuckets: "my-sample-bucket", + logRetentionPeriod: 7 + }; + new ServerlessImageHandler(stack, 'test-serverless-image-handler', props); + // Assertion 1 + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +// -------------------------------------------------------------- +// Custom deployment unit testing +// -------------------------------------------------------------- +test('Custom deployment unit testing', () => { + // Initial Setup + const stack = new Stack(); + const props: ServerlessImageHandlerProps = { + corsEnabled: false, + sourceBuckets: "", + logRetentionPeriod: 7, + autoWebP: true, + customProps: { + lambdaFunctionProps: { + environment: { + TEST_KEY: "TEST_VALUE" + } + }, + cloudFrontDistributionProps: { + enableIpV6: true + }, + apiGatewayProps: { + failOnWarnings: true + }, + bucketPermissions: ['ReadWrite'] + } + }; + const sih = new ServerlessImageHandler(stack, 'test-serverless-image-handler', props); + // Assertion 1 + expect(sih.lambdaFunction()).toBeDefined(); + // Assertion 2 + expect(sih.s3Bucket()).toBeDefined(); + // Assertion 3 + expect(sih.apiGateway()).toBeDefined(); + // Assertion 4 + expect(sih.cloudFrontDistribution()).toBeDefined(); + // Assertion 5 + expect(sih.lambdaFunction()).toHaveProperty('environment.TEST_KEY', 'TEST_VALUE'); +}); \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/tsconfig.json b/source/use_cases/aws-serverless-image-handler/tsconfig.json new file mode 100644 index 000000000..07afb1f64 --- /dev/null +++ b/source/use_cases/aws-serverless-image-handler/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + "charset": "utf8", + "declaration": true, + "experimentalDecorators": true, + "inlineSourceMap": true, + "inlineSources": true, + "lib": [ + "es2018" + ], + "module": "CommonJS", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "target": "ES2018" + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules" + ], + "_generated_by_jsii_": "Generated by jsii - safe to delete, and ideally should be in .gitignore" +} diff --git a/source/use_cases/aws-serverless-web-app/.eslintignore b/source/use_cases/aws-serverless-web-app/.eslintignore new file mode 100644 index 000000000..f48b2d714 --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/.eslintignore @@ -0,0 +1,7 @@ +lib/**/*.js +test/*.js +bin/*.js +*.d.ts +coverage +test/lambda/index.js +cdk.out/**/*.js \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/.gitignore b/source/use_cases/aws-serverless-web-app/.gitignore new file mode 100644 index 000000000..96e33d0f7 --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +bin/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/.npmignore b/source/use_cases/aws-serverless-web-app/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/README.md b/source/use_cases/aws-serverless-web-app/README.md new file mode 100644 index 000000000..f075cfca8 --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/README.md @@ -0,0 +1,36 @@ +# AWS Serverless Web App Use Case + +This use case implements a simple serverless web application that enables users to request unicorn rides from the Wild Rydes fleet. The application will present users with an HTML based user interface for indicating the location where they would like to be picked up and will interface on the backend with a RESTful web service to submit the request and dispatch a nearby unicorn. The application will also provide facilities for users to register with the service and log in before requesting rides. + +## Architecture +The application architecture uses AWS Lambda, Amazon API Gateway, Amazon S3, Amazon DynamoDB, and Amazon Cognito as pictured below: +![Architecture Diagram](architecture.png) + +## Deployment steps +Below are the steps to deploy the use case: + +``` +npm run build + +cdk deploy + +``` + +## Deployment Verification +After the stack is deployed successfully, go to the Outputs tab in AWS Cloudformation console of S3StaticWebsiteStack, it should show the 'websiteURL', click on the link and follow the steps below: + +* Visit /register.html under your website domain, register youself. + +* Verify the registered user email. + +* Visit /ride.html under your website domain. + +* If you are redirected to the sign in page, sign in with the user you created in the previous module. + +* After the map has loaded, click anywhere on the map to set a pickup location. + +* Choose Request Unicorn. You should see a notification in the right sidebar that a unicorn is on its way and then see a unicorn icon fly to your pickup location. + + +*** +© Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/architecture.png b/source/use_cases/aws-serverless-web-app/architecture.png new file mode 100644 index 000000000..319b48ab0 Binary files /dev/null and b/source/use_cases/aws-serverless-web-app/architecture.png differ diff --git a/source/use_cases/aws-serverless-web-app/bin/serverless-web-app.ts b/source/use_cases/aws-serverless-web-app/bin/serverless-web-app.ts new file mode 100644 index 000000000..4b2b4f15a --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/bin/serverless-web-app.ts @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import 'source-map-support/register'; +import * as cdk from '@aws-cdk/core'; +import { S3StaticWebsiteStack } from '../lib/s3-static-site-stack'; +import { ServerlessBackendStack } from '../lib/serverless-backend-stack'; + +const app = new cdk.App(); +const stack1 = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); +const stack2 = new ServerlessBackendStack(app, 'ServerlessBackendStack'); +stack2.addDependency(stack1); +stack1.templateOptions.description = 'Creates a static website using AWS S3 and Amazon Cloudfront'; +stack2.templateOptions.description = 'Creates a serverless backend using Amazon Cognito, Amazon API Gateway, AWS Lambda function and Amazon DynamoDB table'; \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/cdk.json b/source/use_cases/aws-serverless-web-app/cdk.json new file mode 100644 index 000000000..3a62a7677 --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node bin/serverless-web-app.ts" +} \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/lib/lambda/business-logic/index.js b/source/use_cases/aws-serverless-web-app/lib/lambda/business-logic/index.js new file mode 100644 index 000000000..2532f128a --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/lib/lambda/business-logic/index.js @@ -0,0 +1,119 @@ +const randomBytes = require('crypto').randomBytes; + +const AWS = require('aws-sdk'); + +const ddb = new AWS.DynamoDB.DocumentClient(); + +const fleet = [ + { + Name: 'Bucephalus', + Color: 'Golden', + Gender: 'Male', + }, + { + Name: 'Shadowfax', + Color: 'White', + Gender: 'Male', + }, + { + Name: 'Rocinante', + Color: 'Yellow', + Gender: 'Female', + }, +]; + +exports.handler = (event, context, callback) => { + if (!event.requestContext.authorizer) { + errorResponse('Authorization not configured', context.awsRequestId, callback); + return; + } + + const rideId = toUrlString(randomBytes(16)); + console.log('Received event (', rideId, '): ', event); + + // Because we're using a Cognito User Pools authorizer, all of the claims + // included in the authentication token are provided in the request context. + // This includes the username as well as other attributes. + const username = event.requestContext.authorizer.claims['cognito:username']; + + // The body field of the event in a proxy integration is a raw string. + // In order to extract meaningful values, we need to first parse this string + // into an object. A more robust implementation might inspect the Content-Type + // header first and use a different parsing strategy based on that value. + const requestBody = JSON.parse(event.body); + + const pickupLocation = requestBody.PickupLocation; + + const unicorn = findUnicorn(pickupLocation); + + recordRide(rideId, username, unicorn).then(() => { + // You can use the callback function to provide a return value from your Node.js + // Lambda functions. The first parameter is used for failed invocations. The + // second parameter specifies the result data of the invocation. + + // Because this Lambda function is called by an API Gateway proxy integration + // the result object must use the following structure. + callback(null, { + statusCode: 201, + body: JSON.stringify({ + RideId: rideId, + Unicorn: unicorn, + UnicornName: unicorn.Name, + Eta: '30 seconds', + Rider: username, + }), + headers: { + 'Access-Control-Allow-Origin': '*', + }, + }); + }).catch((err) => { + console.error(err); + + // If there is an error during processing, catch it and return + // from the Lambda function successfully. Specify a 500 HTTP status + // code and provide an error message in the body. This will provide a + // more meaningful error response to the end client. + errorResponse(err.message, context.awsRequestId, callback) + }); +}; + +// This is where you would implement logic to find the optimal unicorn for +// this ride (possibly invoking another Lambda function as a microservice.) +// For simplicity, we'll just pick a unicorn at random. +function findUnicorn(pickupLocation) { + console.log('Finding unicorn for ', pickupLocation.Latitude, ', ', pickupLocation.Longitude); + return fleet[Math.floor(Math.random() * fleet.length)]; +} + +function recordRide(rideId, username, unicorn) { + return ddb.put({ + TableName: 'Rides', + Item: { + RideId: rideId, + User: username, + Unicorn: unicorn, + UnicornName: unicorn.Name, + RequestTime: new Date().toISOString(), + }, + }).promise(); +} + +function toUrlString(buffer) { + return buffer.toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +} + +function errorResponse(errorMessage, awsRequestId, callback) { + callback(null, { + statusCode: 500, + body: JSON.stringify({ + Error: errorMessage, + Reference: awsRequestId, + }), + headers: { + 'Access-Control-Allow-Origin': '*', + }, + }); +} diff --git a/source/use_cases/aws-serverless-web-app/lib/lambda/cognito-config/update_s3_object.py b/source/use_cases/aws-serverless-web-app/lib/lambda/cognito-config/update_s3_object.py new file mode 100644 index 000000000..63b6376ed --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/lib/lambda/cognito-config/update_s3_object.py @@ -0,0 +1,46 @@ +import json +import boto3 +import logging +from botocore.exceptions import ClientError + +logger = logging.getLogger() +logger.setLevel(logging.INFO) +s3 = boto3.resource('s3') + +def on_event(event, context): + logger.info("Received event: %s" % json.dumps(event)) + request_type = event['RequestType'] + if request_type == 'Create': return on_create(event) + if request_type == 'Update': return on_create(event) + if request_type == 'Delete': return + raise Exception("Invalid request type: %s" % request_type) + +def on_create(event): + properties = event['ResourceProperties'] + userPoolId = properties['UserPool'] + clientId = properties['Client'] + region = properties['Region'] + bucket = properties['Bucket'] + restapi = properties['RestApi'] + + try: + s3.Object(bucket, 'js/config.js') + config_content = """ + var _config = { + cognito: { + userPoolId: '%s', // e.g. us-east-2_uXboG5pAb + userPoolClientId: '%s', // e.g. 25ddkmj4v6hfsfvruhpfi7n4hv + region: '%s', // e.g. us-east-2 + }, + api: { + invokeUrl: '%s', // e.g. https://rc7nyt4tql.execute-api.us-west-2.amazonaws.com/prod' + } + }; + """ + config_content = config_content % (userPoolId, clientId, region, restapi) + config = s3.Object(bucket,'js/config.js') + config.put(Body=config_content) + except ClientError as e: + logger.error('Error: %s', e) + raise e + return \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/lib/lambda/static-content/copy_s3_objects.py b/source/use_cases/aws-serverless-web-app/lib/lambda/static-content/copy_s3_objects.py new file mode 100644 index 000000000..b31888dbd --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/lib/lambda/static-content/copy_s3_objects.py @@ -0,0 +1,56 @@ +import os +import json +import boto3 +from botocore.exceptions import ClientError +client = boto3.client('s3') + +import logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +def on_event(event, context): + logger.info("Received event: %s" % json.dumps(event)) + request_type = event['RequestType'] + if request_type == 'Create': return on_create(event) + if request_type == 'Update': return on_create(event) + if request_type == 'Delete': return on_delete(event) + raise Exception("Invalid request type: %s" % request_type) + +def on_create(event): + source_bucket = event['ResourceProperties']['SourceBucket'] + source_prefix = event['ResourceProperties'].get('SourcePrefix') or '' + bucket = event['ResourceProperties']['Bucket'] + prefix = event['ResourceProperties'].get('Prefix') or '' + try: + copy_objects(source_bucket, source_prefix, bucket, prefix) + except ClientError as e: + logger.error('Error: %s', e) + raise e + return + +def on_delete(event): + bucket = event['ResourceProperties']['Bucket'] + prefix = event['ResourceProperties'].get('Prefix') or '' + try: + delete_objects(bucket, prefix) + except ClientError as e: + logger.error('Error: %s', e) + raise e + return + +def copy_objects(source_bucket, source_prefix, bucket, prefix): + paginator = client.get_paginator('list_objects_v2') + page_iterator = paginator.paginate(Bucket=source_bucket, Prefix=source_prefix) + for key in {x['Key'] for page in page_iterator for x in page['Contents']}: + dest_key = os.path.join(prefix, os.path.relpath(key, source_prefix)) + if not key.endswith('/'): + #logger.info("copy %s to %s".format(key, dest_key)) + client.copy_object(CopySource={'Bucket': source_bucket, 'Key': key}, Bucket=bucket, Key = dest_key) + return + +def delete_objects(bucket, prefix): + paginator = client.get_paginator('list_objects_v2') + page_iterator = paginator.paginate(Bucket=bucket, Prefix=prefix) + objects = [{'Key': x['Key']} for page in page_iterator for x in page['Contents']] + client.delete_objects(Bucket=bucket, Delete={'Objects': objects}) + return \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/lib/s3-static-site-stack.ts b/source/use_cases/aws-serverless-web-app/lib/s3-static-site-stack.ts new file mode 100644 index 000000000..646f28893 --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/lib/s3-static-site-stack.ts @@ -0,0 +1,82 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { Construct, Stack, StackProps, Duration, CfnOutput } from '@aws-cdk/core'; +import { CloudFrontToS3 } from '@aws-solutions-konstruk/aws-cloudfront-s3'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { Provider } from '@aws-cdk/custom-resources'; +import { CustomResource } from '@aws-cdk/aws-cloudformation'; +import { PolicyStatement } from '@aws-cdk/aws-iam'; + +export class S3StaticWebsiteStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const sourceBucket: string = 'wildrydes-us-east-1'; + const sourcePrefix: string = 'WebApplication/1_StaticWebHosting/website/'; + + const konstruk = new CloudFrontToS3(this, 'CloudFrontToS3', { + deployBucket: true + }); + const targetBucket: string = konstruk.bucket().bucketName; + + const lambdaFunc = new lambda.Function(this, 'staticContentHandler', { + runtime: lambda.Runtime.PYTHON_3_8, + handler: 'copy_s3_objects.on_event', + code: lambda.Code.fromAsset(`${__dirname}/lambda/static-content`), + timeout: Duration.minutes(5), + initialPolicy: [ + new PolicyStatement({ + actions: ["s3:GetObject", + "s3:ListBucket"], + resources: [`arn:aws:s3:::${sourceBucket}`, + `arn:aws:s3:::${sourceBucket}/${sourcePrefix}*`] + }), + new PolicyStatement({ + actions: ["s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl", + "s3:DeleteObject", + "s3:DeleteObjectVersion", + "s3:CopyObject"], + resources: [`arn:aws:s3:::${targetBucket}`, + `arn:aws:s3:::${targetBucket}/*`] + }), + ] + }); + + const customResourceProvider = new Provider(this, 'CustomResourceProvider', { + onEventHandler: lambdaFunc + }); + + new CustomResource(this, 'CustomResource', { + provider: customResourceProvider, + properties: { + SourceBucket: sourceBucket, + SourcePrefix: sourcePrefix, + Bucket: targetBucket + } + }); + + new CfnOutput(this, 'websiteURL', { + value: 'https://' + konstruk.cloudFrontWebDistribution().domainName + }); + + new CfnOutput(this, 'websiteBucket', { + value: targetBucket, + exportName: 'websiteBucket' + }); + } +} \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/lib/serverless-backend-stack.ts b/source/use_cases/aws-serverless-web-app/lib/serverless-backend-stack.ts new file mode 100644 index 000000000..20937d0e6 --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/lib/serverless-backend-stack.ts @@ -0,0 +1,92 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import { CognitoToApiGatewayToLambda } from '@aws-solutions-konstruk/aws-cognito-apigateway-lambda'; +import { LambdaToDynamoDB } from '@aws-solutions-konstruk/aws-lambda-dynamodb'; +import { Construct, Stack, StackProps, Duration, Fn } from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { Provider } from '@aws-cdk/custom-resources'; +import { CustomResource } from '@aws-cdk/aws-cloudformation'; +import { PolicyStatement } from '@aws-cdk/aws-iam'; +import { UserPoolAttribute } from '@aws-cdk/aws-cognito'; +import { Cors } from '@aws-cdk/aws-apigateway'; +import { AttributeType } from '@aws-cdk/aws-dynamodb'; + +export class ServerlessBackendStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const websiteBucketName: string = Fn.importValue('websiteBucket'); + + const konstruk = new CognitoToApiGatewayToLambda(this, 'CognitoToApiGatewayToLambda', { + deployLambda: true, + lambdaFunctionProps: { + code: lambda.Code.asset(`${__dirname}/lambda/business-logic`), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler' + }, + cognitoUserPoolProps: { + userPoolName: 'WileRydes', + autoVerifiedAttributes: [UserPoolAttribute.EMAIL] + }, + apiGatewayProps: { + defaultCorsPreflightOptions: { + allowOrigins: Cors.ALL_ORIGINS, + allowMethods: Cors.ALL_METHODS + } + } + }); + + const lambdaFunc = new lambda.Function(this, 'updateConfigHandler', { + runtime: lambda.Runtime.PYTHON_3_8, + handler: 'update_s3_object.on_event', + code: lambda.Code.fromAsset(`${__dirname}/lambda/cognito-config`), + timeout: Duration.minutes(5), + initialPolicy: [ + new PolicyStatement({ + actions: ["s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl"], + resources: [`arn:aws:s3:::${websiteBucketName}/*`] + }), + ] + }); + + const customResourceProvider = new Provider(this, 'CustomResourceProvider', { + onEventHandler: lambdaFunc + }); + + new CustomResource(this, 'CustomResource', { + provider: customResourceProvider, + properties: { + UserPool: konstruk.userPool().userPoolId, + Client: konstruk.userPoolClient().userPoolClientId, + Region: Stack.of(this).region, + Bucket: websiteBucketName, + RestApi: konstruk.restApi().url + } + }); + + new LambdaToDynamoDB(this, 'LambdaToDynamoDB', { + deployLambda: false, + existingLambdaObj: konstruk.lambdaFunction(), + dynamoTableProps: { + tableName: 'Rides', + partitionKey: { + name: 'RideId', + type: AttributeType.STRING + } + } + }); + } +} \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/package.json b/source/use_cases/aws-serverless-web-app/package.json new file mode 100644 index 000000000..c2b42fe3c --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/package.json @@ -0,0 +1,57 @@ +{ + "name": "@aws-solutions-konstruk/aws-serverless-web-app", + "version": "0.8.0", + "description": "Use case pattern for deploying a serverless web app.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-konstruk.git", + "directory": "source/use_cases/aws-serverless-web-app" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "build+lint+test": "npm run build && npm run lint && npm test && npm run integ-assert" + }, + "dependencies": { + "@aws-solutions-konstruk/aws-cloudfront-s3": "~0.8.0", + "@aws-solutions-konstruk/aws-cognito-apigateway-lambda": "~0.8.0", + "@aws-solutions-konstruk/aws-lambda-dynamodb": "~0.8.0", + "@aws-cdk/core": "~1.25.0", + "@aws-cdk/aws-lambda": "~1.25.0", + "@aws-cdk/aws-cloudfront": "~1.25.0", + "@aws-cdk/aws-s3": "~1.25.0", + "@aws-cdk/custom-resources": "~1.25.0", + "@aws-cdk/aws-cloudformation": "~1.25.0", + "@aws-cdk/aws-iam": "~1.25.0", + "@aws-cdk/aws-cognito": "~1.25.0", + "@aws-cdk/aws-apigateway": "~1.25.0", + "@aws-cdk/aws-dynamodb": "~1.25.0", + "@aws-solutions-konstruk/core": "~0.8.0", + "source-map-support": "^0.5.16" + }, + "devDependencies": { + "@aws-cdk/assert": "~1.25.0", + "@types/jest": "^24.0.23", + "@types/node": "^10.3.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ] + } +} diff --git a/source/use_cases/aws-serverless-web-app/test/__snapshots__/s3-static-site-stack.test.js.snap b/source/use_cases/aws-serverless-web-app/test/__snapshots__/s3-static-site-stack.test.js.snap new file mode 100644 index 000000000..76b778abc --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/test/__snapshots__/s3-static-site-stack.test.js.snap @@ -0,0 +1,634 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`default stack 1`] = ` +Object { + "Outputs": Object { + "websiteBucket": Object { + "Export": Object { + "Name": "websiteBucket", + }, + "Value": Object { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04", + }, + }, + "websiteURL": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E", + "DomainName", + ], + }, + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36ArtifactHash171A5ECB": Object { + "Description": "Artifact hash for asset \\"1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36\\"", + "Type": "String", + }, + "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3BucketE560DEC2": Object { + "Description": "S3 bucket for asset \\"1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36\\"", + "Type": "String", + }, + "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3VersionKeyA9698665": Object { + "Description": "S3 key for asset version \\"1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36\\"", + "Type": "String", + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9ArtifactHash72EE40C1": Object { + "Description": "Artifact hash for asset \\"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\\"", + "Type": "String", + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3Bucket6B4B2C9B": Object { + "Description": "S3 bucket for asset \\"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\\"", + "Type": "String", + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19": Object { + "Description": "S3 key for asset version \\"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\\"", + "Type": "String", + }, + }, + "Resources": Object { + "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E": Object { + "Properties": Object { + "DistributionConfig": Object { + "DefaultCacheBehavior": Object { + "AllowedMethods": Array [ + "GET", + "HEAD", + ], + "CachedMethods": Array [ + "GET", + "HEAD", + ], + "Compress": true, + "ForwardedValues": Object { + "Cookies": Object { + "Forward": "none", + }, + "QueryString": false, + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https", + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": Object { + "Bucket": Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + "RegionalDomainName", + ], + }, + "IncludeCookies": false, + }, + "Origins": Array [ + Object { + "DomainName": Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3S3Bucket9CE6AB04", + "RegionalDomainName", + ], + }, + "Id": "origin1", + "S3OriginConfig": Object { + "OriginAccessIdentity": Object { + "Fn::Join": Array [ + "", + Array [ + "origin-access-identity/cloudfront/", + Object { + "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + }, + ], + ], + }, + }, + }, + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": Object { + "CloudFrontDefaultCertificate": true, + }, + }, + }, + "Type": "AWS::CloudFront::Distribution", + }, + "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91": Object { + "Properties": Object { + "CloudFrontOriginAccessIdentityConfig": Object { + "Comment": "Access S3 bucket content only through CloudFront", + }, + }, + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + }, + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + Object { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "CloudFrontToS3S3Bucket9CE6AB04": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "CloudFrontToS3S3LoggingBucketEF5CD8B2", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "CloudFrontToS3S3BucketPolicy2495300D": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "F16", + "reason": "Public website bucket policy requires a wildcard principal", + }, + ], + }, + }, + "Properties": Object { + "Bucket": Object { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::cloudfront:user/CloudFront Origin Access Identity ", + Object { + "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + }, + ], + ], + }, + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3S3Bucket9CE6AB04", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3S3Bucket9CE6AB04", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + Object { + "Action": "s3:GetObject", + "Effect": "Allow", + "Principal": Object { + "CanonicalUser": Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + "S3CanonicalUserId", + ], + }, + }, + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3S3Bucket9CE6AB04", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "CloudFrontToS3S3LoggingBucketEF5CD8B2": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + Object { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "CustomResource": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "Bucket": Object { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04", + }, + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "CustomResourceProviderframeworkonEvent0AA4376C", + "Arn", + ], + }, + "SourceBucket": "wildrydes-us-east-1", + "SourcePrefix": "WebApplication/1_StaticWebHosting/website/", + }, + "Type": "AWS::CloudFormation::CustomResource", + "UpdateReplacePolicy": "Delete", + }, + "CustomResourceProviderframeworkonEvent0AA4376C": Object { + "DependsOn": Array [ + "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647", + "CustomResourceProviderframeworkonEventServiceRole7EBC5835", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3Bucket6B4B2C9B", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "USER_ON_EVENT_FUNCTION_ARN": Object { + "Fn::GetAtt": Array [ + "staticContentHandlerC21DFC88", + "Arn", + ], + }, + }, + }, + "Handler": "framework.onEvent", + "Role": Object { + "Fn::GetAtt": Array [ + "CustomResourceProviderframeworkonEventServiceRole7EBC5835", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + "Timeout": 900, + }, + "Type": "AWS::Lambda::Function", + }, + "CustomResourceProviderframeworkonEventServiceRole7EBC5835": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "staticContentHandlerC21DFC88", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647", + "Roles": Array [ + Object { + "Ref": "CustomResourceProviderframeworkonEventServiceRole7EBC5835", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "staticContentHandlerC21DFC88": Object { + "DependsOn": Array [ + "staticContentHandlerServiceRoleDefaultPolicy0F5C5865", + "staticContentHandlerServiceRole3B648F21", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3BucketE560DEC2", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3VersionKeyA9698665", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3VersionKeyA9698665", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Handler": "copy_s3_objects.on_event", + "Role": Object { + "Fn::GetAtt": Array [ + "staticContentHandlerServiceRole3B648F21", + "Arn", + ], + }, + "Runtime": "python3.8", + "Timeout": 300, + }, + "Type": "AWS::Lambda::Function", + }, + "staticContentHandlerServiceRole3B648F21": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "staticContentHandlerServiceRoleDefaultPolicy0F5C5865": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:GetObject", + "s3:ListBucket", + ], + "Effect": "Allow", + "Resource": Array [ + "arn:aws:s3:::wildrydes-us-east-1", + "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*", + ], + }, + Object { + "Action": Array [ + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl", + "s3:DeleteObject", + "s3:DeleteObjectVersion", + "s3:CopyObject", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:s3:::", + Object { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04", + }, + ], + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:s3:::", + Object { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04", + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "staticContentHandlerServiceRoleDefaultPolicy0F5C5865", + "Roles": Array [ + Object { + "Ref": "staticContentHandlerServiceRole3B648F21", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + }, +} +`; diff --git a/source/use_cases/aws-serverless-web-app/test/__snapshots__/serverless-backend-stack.test.js.snap b/source/use_cases/aws-serverless-web-app/test/__snapshots__/serverless-backend-stack.test.js.snap new file mode 100644 index 000000000..331872706 --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/test/__snapshots__/serverless-backend-stack.test.js.snap @@ -0,0 +1,1096 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`default stack 1`] = ` +Object { + "Outputs": Object { + "RestApiEndpoint0551178A": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "RestApi0C43BF4B", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791ArtifactHashF30FBC00": Object { + "Description": "Artifact hash for asset \\"3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791\\"", + "Type": "String", + }, + "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3Bucket928903EC": Object { + "Description": "S3 bucket for asset \\"3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791\\"", + "Type": "String", + }, + "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3VersionKey3C7BB3DD": Object { + "Description": "S3 key for asset version \\"3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791\\"", + "Type": "String", + }, + "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360ArtifactHash5ED5576F": Object { + "Description": "Artifact hash for asset \\"9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360\\"", + "Type": "String", + }, + "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3Bucket20EEB389": Object { + "Description": "S3 bucket for asset \\"9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360\\"", + "Type": "String", + }, + "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3VersionKeyC46EC577": Object { + "Description": "S3 key for asset version \\"9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360\\"", + "Type": "String", + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9ArtifactHash72EE40C1": Object { + "Description": "Artifact hash for asset \\"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\\"", + "Type": "String", + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3Bucket6B4B2C9B": Object { + "Description": "S3 bucket for asset \\"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\\"", + "Type": "String", + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19": Object { + "Description": "S3 key for asset version \\"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\\"", + "Type": "String", + }, + }, + "Resources": Object { + "ApiAccessLogGroupCEA70788": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "CognitoAuthorizer": Object { + "Properties": Object { + "IdentitySource": "method.request.header.Authorization", + "Name": "authorizer", + "ProviderARNs": Array [ + Object { + "Fn::GetAtt": Array [ + "CognitoUserPool53E37E69", + "Arn", + ], + }, + ], + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "Type": "COGNITO_USER_POOLS", + }, + "Type": "AWS::ApiGateway::Authorizer", + }, + "CognitoUserPool53E37E69": Object { + "Properties": Object { + "AutoVerifiedAttributes": Array [ + "email", + ], + "LambdaConfig": Object {}, + "UserPoolAddOns": Object { + "AdvancedSecurityMode": "ENFORCED", + }, + "UserPoolName": "WileRydes", + }, + "Type": "AWS::Cognito::UserPool", + }, + "CognitoUserPoolClient5AB59AE4": Object { + "Properties": Object { + "UserPoolId": Object { + "Ref": "CognitoUserPool53E37E69", + }, + }, + "Type": "AWS::Cognito::UserPoolClient", + }, + "CustomResource": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "Bucket": Object { + "Fn::ImportValue": "websiteBucket", + }, + "Client": Object { + "Ref": "CognitoUserPoolClient5AB59AE4", + }, + "Region": Object { + "Ref": "AWS::Region", + }, + "RestApi": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "RestApi0C43BF4B", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/", + ], + ], + }, + "ServiceToken": Object { + "Fn::GetAtt": Array [ + "CustomResourceProviderframeworkonEvent0AA4376C", + "Arn", + ], + }, + "UserPool": Object { + "Ref": "CognitoUserPool53E37E69", + }, + }, + "Type": "AWS::CloudFormation::CustomResource", + "UpdateReplacePolicy": "Delete", + }, + "CustomResourceProviderframeworkonEvent0AA4376C": Object { + "DependsOn": Array [ + "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647", + "CustomResourceProviderframeworkonEventServiceRole7EBC5835", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3Bucket6B4B2C9B", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "USER_ON_EVENT_FUNCTION_ARN": Object { + "Fn::GetAtt": Array [ + "updateConfigHandler59840941", + "Arn", + ], + }, + }, + }, + "Handler": "framework.onEvent", + "Role": Object { + "Fn::GetAtt": Array [ + "CustomResourceProviderframeworkonEventServiceRole7EBC5835", + "Arn", + ], + }, + "Runtime": "nodejs10.x", + "Timeout": 900, + }, + "Type": "AWS::Lambda::Function", + }, + "CustomResourceProviderframeworkonEventServiceRole7EBC5835": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": Object { + "Fn::GetAtt": Array [ + "updateConfigHandler59840941", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647", + "Roles": Array [ + Object { + "Ref": "CustomResourceProviderframeworkonEventServiceRole7EBC5835", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "LambdaFunctionBF21E41F": Object { + "DependsOn": Array [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions.", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3Bucket20EEB389", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3VersionKeyC46EC577", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3VersionKeyC46EC577", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": Object { + "Ref": "LambdaToDynamoDBDynamoTable53C1442D", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn", + ], + }, + "Runtime": "nodejs12.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunctionServiceRole0C4CDE0B": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "LambdaToDynamoDBDynamoTable53C1442D", + "Arn", + ], + }, + Object { + "Ref": "AWS::NoValue", + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": Array [ + Object { + "Ref": "LambdaFunctionServiceRole0C4CDE0B", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "LambdaRestApiAccount": Object { + "DependsOn": Array [ + "RestApi0C43BF4B", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "LambdaRestApiCloudWatchRoleF339D4E6": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaToDynamoDBDynamoTable53C1442D": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "AttributeDefinitions": Array [ + Object { + "AttributeName": "RideId", + "AttributeType": "S", + }, + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": Array [ + Object { + "AttributeName": "RideId", + "KeyType": "HASH", + }, + ], + "SSESpecification": Object { + "SSEEnabled": true, + }, + "TableName": "Rides", + }, + "Type": "AWS::DynamoDB::Table", + "UpdateReplacePolicy": "Retain", + }, + "RestApi0C43BF4B": Object { + "Properties": Object { + "EndpointConfiguration": Object { + "Types": Array [ + "EDGE", + ], + }, + "Name": "RestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "RestApiANYA7C1DC94": Object { + "Properties": Object { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": Object { + "Ref": "CognitoAuthorizer", + }, + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiANYApiPermissionServerlessBackendStackRestApi00D08F58ANYEEE63A3B": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiANYApiPermissionTestServerlessBackendStackRestApi00D08F58ANY9F8B1D52": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiDeployment180EC5030503bead3cacf3da667720719058e886": Object { + "DependsOn": Array [ + "RestApiproxyANY1786B242", + "RestApiproxyOPTIONS32C4B154", + "RestApiproxyC95856DD", + "RestApiANYA7C1DC94", + "RestApiOPTIONS6AA64D2D", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "RestApiDeploymentStageprod3855DE66": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "ApiAccessLogGroupCEA70788", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \\"$context.httpMethod $context.resourcePath $context.protocol\\" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": Object { + "Ref": "RestApiDeployment180EC5030503bead3cacf3da667720719058e886", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "RestApiOPTIONS6AA64D2D": Object { + "Properties": Object { + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": Object { + "IntegrationResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + }, + "StatusCode": "204", + }, + ], + "RequestTemplates": Object { + "application/json": "{ statusCode: 200 }", + }, + "Type": "MOCK", + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Access-Control-Allow-Origin": true, + }, + "StatusCode": "204", + }, + ], + "ResourceId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiUsagePlan6E1C537A": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + "Stage": Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "RestApiproxyANY1786B242": Object { + "Properties": Object { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": Object { + "Ref": "CognitoAuthorizer", + }, + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "RestApiproxyC95856DD", + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "RestApiproxyANYApiPermissionServerlessBackendStackRestApi00D08F58ANYproxy20E632AA": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/", + Object { + "Ref": "RestApiDeploymentStageprod3855DE66", + }, + "/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyANYApiPermissionTestServerlessBackendStackRestApi00D08F58ANYproxyF3BB47AD": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "LambdaFunctionBF21E41F", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "RestApi0C43BF4B", + }, + "/test-invoke-stage/*/{proxy+}", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "RestApiproxyC95856DD": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "RestApi0C43BF4B", + "RootResourceId", + ], + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "RestApiproxyOPTIONS32C4B154": Object { + "Properties": Object { + "AuthorizationType": "NONE", + "HttpMethod": "OPTIONS", + "Integration": Object { + "IntegrationResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + }, + "StatusCode": "204", + }, + ], + "RequestTemplates": Object { + "application/json": "{ statusCode: 200 }", + }, + "Type": "MOCK", + }, + "MethodResponses": Array [ + Object { + "ResponseParameters": Object { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Access-Control-Allow-Origin": true, + }, + "StatusCode": "204", + }, + ], + "ResourceId": Object { + "Ref": "RestApiproxyC95856DD", + }, + "RestApiId": Object { + "Ref": "RestApi0C43BF4B", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "updateConfigHandler59840941": Object { + "DependsOn": Array [ + "updateConfigHandlerServiceRoleDefaultPolicy157F28C3", + "updateConfigHandlerServiceRole3B176B96", + ], + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3Bucket928903EC", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3VersionKey3C7BB3DD", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3VersionKey3C7BB3DD", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Handler": "update_s3_object.on_event", + "Role": Object { + "Fn::GetAtt": Array [ + "updateConfigHandlerServiceRole3B176B96", + "Arn", + ], + }, + "Runtime": "python3.8", + "Timeout": 300, + }, + "Type": "AWS::Lambda::Function", + }, + "updateConfigHandlerServiceRole3B176B96": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "updateConfigHandlerServiceRoleDefaultPolicy157F28C3": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:s3:::", + Object { + "Fn::ImportValue": "websiteBucket", + }, + "/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "updateConfigHandlerServiceRoleDefaultPolicy157F28C3", + "Roles": Array [ + Object { + "Ref": "updateConfigHandlerServiceRole3B176B96", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + }, +} +`; diff --git a/source/use_cases/aws-serverless-web-app/test/integ.backend-deployment.expected.json b/source/use_cases/aws-serverless-web-app/test/integ.backend-deployment.expected.json new file mode 100644 index 000000000..b626e9eb7 --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/test/integ.backend-deployment.expected.json @@ -0,0 +1,1092 @@ +{ + "Resources": { + "LambdaFunctionServiceRole0C4CDE0B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "LambdaFunctionServiceRoleDefaultPolicy126C8897": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaToDynamoDBDynamoTable53C1442D", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRole0C4CDE0B" + } + ] + } + }, + "LambdaFunctionBF21E41F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3Bucket20EEB389" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3VersionKeyC46EC577" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3VersionKeyC46EC577" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRole0C4CDE0B", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": { + "Ref": "LambdaToDynamoDBDynamoTable53C1442D" + } + } + } + }, + "DependsOn": [ + "LambdaFunctionServiceRoleDefaultPolicy126C8897", + "LambdaFunctionServiceRole0C4CDE0B" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with more tighter permissions." + } + ] + } + } + }, + "RestApi0C43BF4B": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types": [ + "EDGE" + ] + }, + "Name": "RestApi" + } + }, + "RestApiDeployment180EC5030503bead3cacf3da667720719058e886": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "RestApiproxyANY1786B242", + "RestApiproxyOPTIONS32C4B154", + "RestApiproxyC95856DD", + "RestApiANYA7C1DC94", + "RestApiOPTIONS6AA64D2D" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource" + } + ] + } + } + }, + "RestApiDeploymentStageprod3855DE66": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "ApiAccessLogGroupCEA70788", + "Arn" + ] + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + }, + "DeploymentId": { + "Ref": "RestApiDeployment180EC5030503bead3cacf3da667720719058e886" + }, + "MethodSettings": [ + { + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + } + ], + "StageName": "prod" + } + }, + "RestApiOPTIONS6AA64D2D": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "OPTIONS", + "ResourceId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ] + } + }, + "RestApiproxyC95856DD": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "PathPart": "{proxy+}", + "RestApiId": { + "Ref": "RestApi0C43BF4B" + } + } + }, + "RestApiproxyOPTIONS32C4B154": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "OPTIONS", + "ResourceId": { + "Ref": "RestApiproxyC95856DD" + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'" + }, + "StatusCode": "204" + } + ], + "RequestTemplates": { + "application/json": "{ statusCode: 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "ResponseParameters": { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + "StatusCode": "204" + } + ] + } + }, + "RestApiproxyANYApiPermissionServerlessBackendStackRestApi00D08F58ANYproxy20E632AA": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/*/{proxy+}" + ] + ] + } + } + }, + "RestApiproxyANYApiPermissionTestServerlessBackendStackRestApi00D08F58ANYproxyF3BB47AD": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/test-invoke-stage/*/{proxy+}" + ] + ] + } + } + }, + "RestApiproxyANY1786B242": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Ref": "RestApiproxyC95856DD" + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": { + "Ref": "CognitoAuthorizer" + }, + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "RestApiANYApiPermissionServerlessBackendStackRestApi00D08F58ANYEEE63A3B": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/*/" + ] + ] + } + } + }, + "RestApiANYApiPermissionTestServerlessBackendStackRestApi00D08F58ANY9F8B1D52": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "RestApi0C43BF4B" + }, + "/test-invoke-stage/*/" + ] + ] + } + } + }, + "RestApiANYA7C1DC94": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "RestApi0C43BF4B", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": { + "Ref": "CognitoAuthorizer" + }, + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "RestApiUsagePlan6E1C537A": { + "Type": "AWS::ApiGateway::UsagePlan", + "Properties": { + "ApiStages": [ + { + "ApiId": { + "Ref": "RestApi0C43BF4B" + }, + "Stage": { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "Throttle": {} + } + ] + } + }, + "ApiAccessLogGroupCEA70788": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "LambdaRestApiCloudWatchRoleF339D4E6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy" + } + ] + } + }, + "LambdaRestApiAccount": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "LambdaRestApiCloudWatchRoleF339D4E6", + "Arn" + ] + } + }, + "DependsOn": [ + "RestApi0C43BF4B" + ] + }, + "CognitoUserPool53E37E69": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AutoVerifiedAttributes": [ + "email" + ], + "LambdaConfig": {}, + "UserPoolAddOns": { + "AdvancedSecurityMode": "ENFORCED" + }, + "UserPoolName": "WileRydes" + } + }, + "CognitoUserPoolClient5AB59AE4": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "CognitoUserPool53E37E69" + } + } + }, + "CognitoAuthorizer": { + "Type": "AWS::ApiGateway::Authorizer", + "Properties": { + "RestApiId": { + "Ref": "RestApi0C43BF4B" + }, + "Type": "COGNITO_USER_POOLS", + "IdentitySource": "method.request.header.Authorization", + "Name": "authorizer", + "ProviderARNs": [ + { + "Fn::GetAtt": [ + "CognitoUserPool53E37E69", + "Arn" + ] + } + ] + } + }, + "updateConfigHandlerServiceRole3B176B96": { + "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" + ] + ] + } + ] + } + }, + "updateConfigHandlerServiceRoleDefaultPolicy157F28C3": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Fn::ImportValue": "websiteBucket" + }, + "/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "updateConfigHandlerServiceRoleDefaultPolicy157F28C3", + "Roles": [ + { + "Ref": "updateConfigHandlerServiceRole3B176B96" + } + ] + } + }, + "updateConfigHandler59840941": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3Bucket928903EC" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3VersionKey3C7BB3DD" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3VersionKey3C7BB3DD" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "update_s3_object.on_event", + "Role": { + "Fn::GetAtt": [ + "updateConfigHandlerServiceRole3B176B96", + "Arn" + ] + }, + "Runtime": "python3.8", + "Timeout": 300 + }, + "DependsOn": [ + "updateConfigHandlerServiceRoleDefaultPolicy157F28C3", + "updateConfigHandlerServiceRole3B176B96" + ] + }, + "CustomResourceProviderframeworkonEventServiceRole7EBC5835": { + "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" + ] + ] + } + ] + } + }, + "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "updateConfigHandler59840941", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647", + "Roles": [ + { + "Ref": "CustomResourceProviderframeworkonEventServiceRole7EBC5835" + } + ] + } + }, + "CustomResourceProviderframeworkonEvent0AA4376C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3Bucket6B4B2C9B" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "framework.onEvent", + "Role": { + "Fn::GetAtt": [ + "CustomResourceProviderframeworkonEventServiceRole7EBC5835", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "updateConfigHandler59840941", + "Arn" + ] + } + } + }, + "Timeout": 900 + }, + "DependsOn": [ + "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647", + "CustomResourceProviderframeworkonEventServiceRole7EBC5835" + ] + }, + "CustomResource": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomResourceProviderframeworkonEvent0AA4376C", + "Arn" + ] + }, + "UserPool": { + "Ref": "CognitoUserPool53E37E69" + }, + "Client": { + "Ref": "CognitoUserPoolClient5AB59AE4" + }, + "Region": { + "Ref": "AWS::Region" + }, + "Bucket": { + "Fn::ImportValue": "websiteBucket" + }, + "RestApi": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "RestApi0C43BF4B" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/" + ] + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LambdaToDynamoDBDynamoTable53C1442D": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "RideId", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "RideId", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "SSESpecification": { + "SSEEnabled": true + }, + "TableName": "Rides" + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + } + }, + "Parameters": { + "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3Bucket20EEB389": { + "Type": "String", + "Description": "S3 bucket for asset \"9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360\"" + }, + "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3VersionKeyC46EC577": { + "Type": "String", + "Description": "S3 key for asset version \"9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360\"" + }, + "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360ArtifactHash5ED5576F": { + "Type": "String", + "Description": "Artifact hash for asset \"9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360\"" + }, + "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3Bucket928903EC": { + "Type": "String", + "Description": "S3 bucket for asset \"3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791\"" + }, + "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3VersionKey3C7BB3DD": { + "Type": "String", + "Description": "S3 key for asset version \"3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791\"" + }, + "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791ArtifactHashF30FBC00": { + "Type": "String", + "Description": "Artifact hash for asset \"3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791\"" + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3Bucket6B4B2C9B": { + "Type": "String", + "Description": "S3 bucket for asset \"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\"" + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19": { + "Type": "String", + "Description": "S3 key for asset version \"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\"" + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9ArtifactHash72EE40C1": { + "Type": "String", + "Description": "Artifact hash for asset \"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\"" + } + }, + "Outputs": { + "RestApiEndpoint0551178A": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "RestApi0C43BF4B" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "RestApiDeploymentStageprod3855DE66" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/test/integ.backend-deployment.ts b/source/use_cases/aws-serverless-web-app/test/integ.backend-deployment.ts new file mode 100644 index 000000000..765dd43c4 --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/test/integ.backend-deployment.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from '@aws-cdk/core'; +import { ServerlessBackendStack } from '../lib/serverless-backend-stack'; + +const app = new cdk.App(); +new ServerlessBackendStack(app, 'ServerlessBackendStack'); \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/test/integ.s3-static-website-deployment.expected.json b/source/use_cases/aws-serverless-web-app/test/integ.s3-static-website-deployment.expected.json new file mode 100644 index 000000000..f59d7e0df --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/test/integ.s3-static-website-deployment.expected.json @@ -0,0 +1,630 @@ +{ + "Resources": { + "CloudFrontToS3S3LoggingBucketEF5CD8B2": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + }, + { + "id": "W51", + "reason": "This S3 bucket Bucket does not need a bucket policy" + } + ] + } + } + }, + "CloudFrontToS3S3Bucket9CE6AB04": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "CloudFrontToS3S3LoggingBucketEF5CD8B2" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "CloudFrontToS3S3BucketPolicy2495300D": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::cloudfront:user/CloudFront Origin Access Identity ", + { + "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91" + } + ] + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CloudFrontToS3S3Bucket9CE6AB04", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CloudFrontToS3S3Bucket9CE6AB04", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Principal": { + "CanonicalUser": { + "Fn::GetAtt": [ + "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + "S3CanonicalUserId" + ] + } + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CloudFrontToS3S3Bucket9CE6AB04", + "Arn" + ] + }, + "/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "F16", + "reason": "Public website bucket policy requires a wildcard principal" + } + ] + } + } + }, + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + }, + { + "id": "W51", + "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" + } + ] + } + } + }, + "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91": { + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + "Properties": { + "CloudFrontOriginAccessIdentityConfig": { + "Comment": "Access S3 bucket content only through CloudFront" + } + } + }, + "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "AllowedMethods": [ + "GET", + "HEAD" + ], + "CachedMethods": [ + "GET", + "HEAD" + ], + "Compress": true, + "ForwardedValues": { + "Cookies": { + "Forward": "none" + }, + "QueryString": false + }, + "TargetOriginId": "origin1", + "ViewerProtocolPolicy": "redirect-to-https" + }, + "DefaultRootObject": "index.html", + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Logging": { + "Bucket": { + "Fn::GetAtt": [ + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + "RegionalDomainName" + ] + }, + "IncludeCookies": false + }, + "Origins": [ + { + "DomainName": { + "Fn::GetAtt": [ + "CloudFrontToS3S3Bucket9CE6AB04", + "RegionalDomainName" + ] + }, + "Id": "origin1", + "S3OriginConfig": { + "OriginAccessIdentity": { + "Fn::Join": [ + "", + [ + "origin-access-identity/cloudfront/", + { + "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91" + } + ] + ] + } + } + } + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": { + "CloudFrontDefaultCertificate": true + } + } + } + }, + "staticContentHandlerServiceRole3B648F21": { + "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" + ] + ] + } + ] + } + }, + "staticContentHandlerServiceRoleDefaultPolicy0F5C5865": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject", + "s3:ListBucket" + ], + "Effect": "Allow", + "Resource": [ + "arn:aws:s3:::wildrydes-us-east-1", + "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*" + ] + }, + { + "Action": [ + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl", + "s3:DeleteObject", + "s3:DeleteObjectVersion", + "s3:CopyObject" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "staticContentHandlerServiceRoleDefaultPolicy0F5C5865", + "Roles": [ + { + "Ref": "staticContentHandlerServiceRole3B648F21" + } + ] + } + }, + "staticContentHandlerC21DFC88": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3BucketE560DEC2" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3VersionKeyA9698665" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3VersionKeyA9698665" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "copy_s3_objects.on_event", + "Role": { + "Fn::GetAtt": [ + "staticContentHandlerServiceRole3B648F21", + "Arn" + ] + }, + "Runtime": "python3.8", + "Timeout": 300 + }, + "DependsOn": [ + "staticContentHandlerServiceRoleDefaultPolicy0F5C5865", + "staticContentHandlerServiceRole3B648F21" + ] + }, + "CustomResourceProviderframeworkonEventServiceRole7EBC5835": { + "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" + ] + ] + } + ] + } + }, + "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "staticContentHandlerC21DFC88", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647", + "Roles": [ + { + "Ref": "CustomResourceProviderframeworkonEventServiceRole7EBC5835" + } + ] + } + }, + "CustomResourceProviderframeworkonEvent0AA4376C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3Bucket6B4B2C9B" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "framework.onEvent", + "Role": { + "Fn::GetAtt": [ + "CustomResourceProviderframeworkonEventServiceRole7EBC5835", + "Arn" + ] + }, + "Runtime": "nodejs10.x", + "Environment": { + "Variables": { + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "staticContentHandlerC21DFC88", + "Arn" + ] + } + } + }, + "Timeout": 900 + }, + "DependsOn": [ + "CustomResourceProviderframeworkonEventServiceRoleDefaultPolicy93CD1647", + "CustomResourceProviderframeworkonEventServiceRole7EBC5835" + ] + }, + "CustomResource": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomResourceProviderframeworkonEvent0AA4376C", + "Arn" + ] + }, + "SourceBucket": "wildrydes-us-east-1", + "SourcePrefix": "WebApplication/1_StaticWebHosting/website/", + "Bucket": { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + }, + "Parameters": { + "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3BucketE560DEC2": { + "Type": "String", + "Description": "S3 bucket for asset \"1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36\"" + }, + "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3VersionKeyA9698665": { + "Type": "String", + "Description": "S3 key for asset version \"1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36\"" + }, + "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36ArtifactHash171A5ECB": { + "Type": "String", + "Description": "Artifact hash for asset \"1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36\"" + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3Bucket6B4B2C9B": { + "Type": "String", + "Description": "S3 bucket for asset \"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\"" + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9S3VersionKey8971BB19": { + "Type": "String", + "Description": "S3 key for asset version \"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\"" + }, + "AssetParametersf587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9ArtifactHash72EE40C1": { + "Type": "String", + "Description": "Artifact hash for asset \"f587c683163dea7b70b883fe8f803ffe0622a40e05b3766e08ffa9ed25caabc9\"" + } + }, + "Outputs": { + "websiteURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E", + "DomainName" + ] + } + ] + ] + } + }, + "websiteBucket": { + "Value": { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + }, + "Export": { + "Name": "websiteBucket" + } + } + } +} \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/test/integ.s3-static-website-deployment.ts b/source/use_cases/aws-serverless-web-app/test/integ.s3-static-website-deployment.ts new file mode 100644 index 000000000..6e8a10593 --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/test/integ.s3-static-website-deployment.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from '@aws-cdk/core'; +import { S3StaticWebsiteStack } from '../lib/s3-static-site-stack'; + +const app = new cdk.App(); +new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/test/s3-static-site-stack.test.ts b/source/use_cases/aws-serverless-web-app/test/s3-static-site-stack.test.ts new file mode 100644 index 000000000..acf86147d --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/test/s3-static-site-stack.test.ts @@ -0,0 +1,113 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from '@aws-cdk/core'; +import { S3StaticWebsiteStack } from '../lib/s3-static-site-stack'; +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +test('default stack', () => { + const app = new cdk.App(); + const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check s3 bucket encryption setting', () => { + const app = new cdk.App(); + const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); + expect(stack).toHaveResource("AWS::S3::Bucket", { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + SSEAlgorithm: "AES256" + } + } + ] + } + }); +}); + +test('check s3 bucket public access setting', () => { + const app = new cdk.App(); + const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); + expect(stack).toHaveResource("AWS::S3::Bucket", { + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true + } + }); +}); + +test('check CR lambda function permissions', () => { + const app = new cdk.App(); + const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); + expect(stack).toHaveResource("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "s3:GetObject", + "s3:ListBucket" + ], + Effect: "Allow", + Resource: [ + "arn:aws:s3:::wildrydes-us-east-1", + "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*" + ] + }, + { + Action: [ + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl", + "s3:DeleteObject", + "s3:DeleteObjectVersion", + "s3:CopyObject" + ], + Effect: "Allow", + Resource: [ + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + Ref: "CloudFrontToS3S3Bucket9CE6AB04" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + Ref: "CloudFrontToS3S3Bucket9CE6AB04" + }, + "/*" + ] + ] + } + ] + } + ], + Version: "2012-10-17" + } + }); +}); \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/test/serverless-backend-stack.test.ts b/source/use_cases/aws-serverless-web-app/test/serverless-backend-stack.test.ts new file mode 100644 index 000000000..5c76a4548 --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/test/serverless-backend-stack.test.ts @@ -0,0 +1,79 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as cdk from '@aws-cdk/core'; +import { ServerlessBackendStack } from '../lib/serverless-backend-stack'; +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; + +test('default stack', () => { + const app = new cdk.App(); + const stack = new ServerlessBackendStack(app, 'ServerlessBackendStack'); + expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); +}); + +test('check Api Method CORS setting for HTTP OPTIONS method', () => { + const app = new cdk.App(); + const stack = new ServerlessBackendStack(app, 'ServerlessBackendStack'); + expect(stack).toHaveResource("AWS::ApiGateway::Method", { + HttpMethod: "OPTIONS", + AuthorizationType: "NONE", + MethodResponses: [ + { + ResponseParameters: { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Origin": true, + "method.response.header.Access-Control-Allow-Methods": true + }, + StatusCode: "204" + } + ] + }); +}); + +test('check lambda permissions', () => { + const app = new cdk.App(); + const stack = new ServerlessBackendStack(app, 'ServerlessBackendStack'); + expect(stack).toHaveResource("AWS::Lambda::Permission", { + Action: "lambda:InvokeFunction", + Principal: "apigateway.amazonaws.com", + SourceArn: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":execute-api:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":", + { + Ref: "RestApi0C43BF4B" + }, + "/", + { + Ref: "RestApiDeploymentStageprod3855DE66" + }, + "/*/" + ] + ] + } + }); +}); \ No newline at end of file diff --git a/source/use_cases/aws-serverless-web-app/tsconfig.json b/source/use_cases/aws-serverless-web-app/tsconfig.json new file mode 100644 index 000000000..10250a8e6 --- /dev/null +++ b/source/use_cases/aws-serverless-web-app/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + "charset": "utf8", + "declaration": true, + "experimentalDecorators": true, + "inlineSourceMap": true, + "inlineSources": true, + "lib": [ "es2018" ], + "module": "CommonJS", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "target": "ES2018" + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules","cdk.out" + ] +} diff --git a/source/use_cases/eslintrc.yml b/source/use_cases/eslintrc.yml new file mode 100644 index 000000000..6806f7be5 --- /dev/null +++ b/source/use_cases/eslintrc.yml @@ -0,0 +1,49 @@ +--- +env: + jest: true + node: true + +plugins: + - '@typescript-eslint' + - import + - license-header + +parser: '@typescript-eslint/parser' +parserOptions: + ecmaVersion: 2018 + sourceType: module + project: ./tsconfig.json + +extends: + - plugin:import/typescript + +settings: + import/parsers: + '@typescript-eslint/parser': ['.ts', '.tsx'] + import/resolver: + node: {} + typescript: + directory: ./tsconfig.json + +rules: + # Require use of the `import { foo } from 'bar';` form instead of `import foo = require('bar');` + '@typescript-eslint/no-require-imports': + - error + + # Require all imported dependencies are actually declared in package.json + 'import/no-extraneous-dependencies': + - error + - devDependencies: # Only allow importing devDependencies from: + - '**/test/**' # --> Unit tests + - '**/utils.ts' # --> uses deepmerge + optionalDependencies: false # Disallow importing optional dependencies (those shouldn't be in use in the project) + peerDependencies: false # Disallow importing peer dependencies (that aren't also direct dependencies) + + # Require all imported libraries actually resolve (!!required for import/no-extraneous-dependencies to work!!) + 'import/no-unresolved': + - error + + #Check for license header + 'license-header/header': + - error + - ../license-header.js diff --git a/source/use_cases/license-header.js b/source/use_cases/license-header.js new file mode 100644 index 000000000..e3d94bb9f --- /dev/null +++ b/source/use_cases/license-header.js @@ -0,0 +1,12 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ \ No newline at end of file