├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs └── images │ └── lambda-formation.png ├── index.js ├── lib ├── logger │ └── index.js ├── project │ ├── handler.js │ └── index.js ├── resource │ ├── create.js │ ├── delete.js │ ├── handler.js │ ├── index.js │ └── update.js └── util │ ├── done.js │ ├── getErrorString.js │ ├── getPhysicalResourceId.js │ ├── getRequestType.js │ ├── index.js │ └── normalizeEvent.js ├── package.json └── test ├── fixtures └── project1 │ ├── index.js │ └── lib │ └── resources │ ├── resource1 │ ├── create.js │ ├── delete.js │ ├── index.js │ └── update.js │ ├── resource2 │ ├── create.js │ ├── delete.js │ └── update.js │ └── resource3 │ ├── create.js │ ├── delete.js │ ├── index.js │ └── update.js ├── integration └── project1 │ ├── project.test.js │ ├── resource1.test.js │ ├── resource1_create.test.js │ ├── resource1_delete.test.js │ ├── resource1_update.test.js │ ├── resource2_create.test.js │ ├── resource2_delete.test.js │ └── resource2_update.test.js └── unit ├── done.test.js ├── getErrorString.test.js ├── load.test.js ├── logger.test.js └── normalizeEvent.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | coverage 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6.10.2 4 | - 4.3.2 5 | after_script: NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha 6 | --report lcovonly -- -R spec --timeout 15000 --recursive test/ && cat ./coverage/lcov.info 7 | | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 8 | deploy: 9 | provider: npm 10 | email: as.us.labs@sungardas.com 11 | api_key: 12 | secure: EGiTvocsnCwI6MgB+/NBDECbQs3mu38WilzA/QbfTEdD9Z+g2xKykr5KZrnBmcuC+6/XI2liASLeymHayPWBVnPheGQyuxgSS5uvIVYb9jU05X7v9s/jiYF83yfhzTLx8/elJV82BRj94u3Cq35gGl64kUgV658aRiAVbCaUxpeLIRkwc07l6JKEJ7egOe2jheNjHN5wKFKRrmlqxcUvgoNDvE+21bI1PnHbbIAgXLT6cnRQL4/nJnK55uJOXEb8cVUc6mPoHpO+EwqI3zzIttBfF/Aouw86638H2eTJFEJEiF4hOOYrfK68+FXKwobDrz3a1J8d44OaVNX3MA0JTSt2blb/r9GLobwa36qnIrmZ+E6qJHJtekJFTE7m7JG6NegXb++NW/8aPs10x3HL+XfbsBGyvoRDjtBEz1aTr1Nr8Uy5MujviF7dDEIk1uCYdIMuA2nzspSdMJg+6cz/yj3xe0vDactTgU44WhRiYVjtjEn9AGO6+S0Wa2+RzgT2jOUWoDX08GPme7cLFLKXMx31lEs5gydhVd3x7dciXDZE6MmCArNXE7aaTCzWhi/2hjapfxwnt4yWggRspJJdC7srQpdPw8jjTEdCEkNBVpVWXjiK95mwyqDWV3ahIXtlCx6Ua37lS2Oxlyf5/gpOEEf+WiL4bshAi7IEVlRDswI= 13 | on: 14 | tags: true 15 | repo: SungardAS/lambda-formation 16 | node: 6.10.2 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented here in 3 | accordance with [Keep a CHANGELOG][keep-changelog-url]. 4 | This project adheres to [Semantic Versioning][semver-url]. 5 | 6 | ## [0.1.11] - 2017-06-02 7 | ### Added 8 | - Logging [#10] [estahn] 9 | 10 | ## [0.1.10] - 2017-05-15 11 | ### Fixed 12 | - CloudFormation `Reason` should always be a string 13 | 14 | ## [0.1.9] - 2017-01-09 15 | ### Fixed 16 | - Always set PhysicalId if it exists. Fixes issues with updates that 17 | incorrectly change the ID when null is returned 18 | 19 | ## [0.1.8] - 2016-11-20 20 | ### Added 21 | - Accept and pass through options for cfn-responder 22 | 23 | ## [0.1.7] - 2016-06-18 24 | ### Fixed 25 | - typo in package.json 26 | 27 | ## [0.1.6] - 2016-06-18 28 | ### Added 29 | - Handlers detect if a message is from SNS and will normailze the event 30 | object accordingly 31 | - Updated dependencies and devDependencies 32 | 33 | ## [0.1.5] - 2016-04-12 34 | ### Fixed 35 | - removed devDependencies from dependencies 36 | - use nock ^8.0.0 37 | 38 | ## [0.1.4] - 2016-04-12 39 | ### Changed 40 | - updated cfn-responder version to 0.1.5 41 | - updated tests to reflect error returned from cfn-responder 42 | - updated modules in package.json to latest versions 43 | 44 | [keep-changelog-url]: http://keepachangelog.com/ 45 | [semver-url]: http://semver.org/ 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lambda-formation 2 | 3 | A small framework for building nodejs [AWS Lambda][aws-lambda-url] 4 | projects that are compatible with [AWS CloudFormation][aws-cloudformation-url] 5 | [Custom Resources][aws-custom-resources-url]. 6 | 7 | [![lambda-formation][lambda-formation-image]][lambda-formation-url] 8 | 9 | [![NPM version][npm-image]][npm-url] 10 | [![Build Status][travis-image]][travis-url] 11 | [![Dependency Status][daviddm-image]][daviddm-url] 12 | [![Coverage percentage][coveralls-image]][coveralls-url] 13 | [![Code Climate][codeclimate-image]][codeclimate-url] 14 | 15 | 16 | The functions in a lambda-formation project can also be run directly 17 | through the AWS Lambda API. They will work with but do not rely on AWS 18 | CloudFormation. 19 | 20 | ## Getting Started 21 | 22 | Use the Yeoman generator 23 | [generator-lambda-formation](https://github.com/SungardAS/generator-lambda-formation) 24 | 25 | yo lambda-formation:project my-lambda-formation-project 26 | 27 | Now lets create a resource: 28 | 29 | cd my-lambda-formation-project 30 | 31 | yo lambda-formation:resource resource1 32 | 33 | Now you have a project structure similar to the following: 34 | 35 | - my-lambda-formation-project 36 | |- .gitignore 37 | |- LICENSE 38 | |- README.md 39 | |- index.js 40 | |- lib 41 | |- resources 42 | |- README.md 43 | |- resource1 44 | |- create.js 45 | |- delete.js 46 | |- index.js 47 | |- update.js 48 | |- package.json 49 | 50 | 51 | Add as many resources as necessary. A resource will map directly to a 52 | CloudFormation Custom Resource. Beneath each resource a skeleton for controling `Create`, 53 | `Update` and `Delete` request types have be generated. 54 | 55 | 56 | ## How does it work? 57 | 58 | The project is designed so that calling a function from CloudFormation 59 | is as seamless as possible. 60 | 61 | For the following Custom Resource definition: 62 | 63 | { 64 | "AWSTemplateFormatVersion": "2010-09-09", 65 | "Resources": { 66 | "MyLambdaFormationResource": { 67 | "Type": "Custom::resource1", 68 | "Properties": { 69 | "ServiceToken": ARN_OF_LAMBDA_FORMATION, 70 | "myFirstProperty": "First Property", 71 | "mySecondProperty": { 72 | "name": "A Sub Property" 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | CloudFormation will run the lamda code at `ARN_OF_LAMBDA_FORMATION`, 80 | which is the zip of the lambda-formation project. 81 | 82 | In this example lambda-formation will load `resource1` based on the 83 | `Custom::resource1` type. If this project had multiple resources any 84 | other resource could be executed by NAME with `Custom::NAME`. 85 | 86 | If this is the first execution of the CloudFormation stack the handler in `create.js` 87 | will be executed. When the stack is updated the handler in `update.js` is executed. 88 | When the stack is removed the handler in `delete.js` is executed. 89 | 90 | 91 | ### Direct call through Lambda 92 | 93 | While the structure is designed to work with CloudFormation any handler 94 | can be called directly though Lambda. 95 | 96 | The main handler in the root `index.js` file is a router that can get 97 | to any resource in the project. 98 | 99 | Parameters to the main handler support multiple cases for the first 100 | letter. CloudFormation defaults to capitals while it is convention for 101 | most other node project to use lower case. This project supports both. 102 | 103 | `resourceType|ResourceType` - Name of the resource to load. Will 104 | forward to the handler in the `index.js` file of the named resource. 105 | 106 | `requestType|RequestType` - Name for the type of request 107 | `Create|Update|Delete`. Can also be one of `create|update|delete` 108 | 109 | Direct Lambda execution: 110 | 111 | { 112 | "requestType": "delete", 113 | "resourceType": "resource1", 114 | "myCustomParam": "myCustomValue" 115 | } 116 | 117 | ### Cross-Region Support with SNS 118 | 119 | For regions that do not support Lambda, an SNS topic can be set as the 120 | `ServiceToken` in the CloudFormation template. 121 | 122 | #### Example 123 | 124 | Run a CloudFormation Template `sa-east-1` (as of this release does not 125 | support Lambda) that uses lambda-formation resources in 126 | `us-east-1`. 127 | 128 | 1. Upload the lambda-formation project to us-east-1 (or other Lambda 129 | supported region) 130 | 2. Create an SNS Topic in `sa-east-1` 131 | 3. Subscribe the lambda-formation function from step #1 to the SNS Topic 132 | in step #2 133 | 4. Launch CloudFormation in `sa-east-1` with the `ServiceToken` set to 134 | the SNS Topic set in step #2 135 | 136 | lambda-formation will take care of processing the SNS message and 137 | responding to the CloudFormation stack 138 | 139 | ## Where does my code go? 140 | 141 | The `create.js`, `update.js` and `delete.js` files under each resource 142 | in `lib/resources` is where any code or modules should be added. 143 | 144 | Add code directly to the stubbed functions or write a module 145 | and require it here. Please do not alter the `handler` function. 146 | This function controls the routing and execution response based on 147 | direct Lambda versus CloudFormation calls. 148 | 149 | ### Resources 150 | 151 | Each resource file will have a `create`, `update` or `delete` funciton 152 | respectively. The only requirement is that `util.done` is called when 153 | any custom code has finish processing. The `util.done` function will 154 | prepare the responce for CloudFormation or as a return directly to 155 | Lambda and complete the context for the execution. 156 | 157 | util.done(err,event,context[,data,id]); 158 | 159 | On success `err` should be `null` 160 | 161 | The `event` and `context` objects are required and can simply be passed 162 | through. 163 | 164 | The `data` parameter is optional and if included must be null or an object of key/value pairs 165 | that describe resource. When executed though CloudFormation the 166 | keys will be availbale as Resource Outputs. For direct Lambda calls the 167 | object will be sent to `context.done`. 168 | 169 | The `id` parameter is only required for 'create.js'. This will be 170 | the ID CloudFormation will use to track the resource. If `id` is 171 | provided then `data` must also be defined. 172 | 173 | ### Logging 174 | 175 | Logging is pre-configured via the [winston][winston-github-url] library and can be included 176 | via lambda-formation: 177 | 178 | var logger = require('lambda-formation').logger; 179 | 180 | Within your code use the winston shortcut methods `log`, `info`, `debug`: 181 | 182 | logger.log('info', 'My messages'); 183 | logger.info('My message'); 184 | logger.debug('My message'); 185 | ... 186 | 187 | Framework internal logs will be logged with log-level `debug`. 188 | To enable them to be logged to CloudWatch set the environment variable `CFN_LOG_LEVEL` to `debug`. 189 | 190 | ## Examples 191 | 192 | * [lambda-formation-example-resources](https://github.com/SungardAS/lambda-formation-example-resources) 193 | * [spotinst-lambda](https://github.com/SungardAS/spotinst-lambda) 194 | 195 | ## Sungard Availability Services | Labs 196 | [![Sungard Availability Services | Labs][labs-logo]][labs-github-url] 197 | 198 | This project is maintained by the Labs team at [Sungard Availability 199 | Services](http://sungardas.com) 200 | 201 | GitHub: [https://sungardas.github.io](https://sungardas.github.io) 202 | 203 | Blog: 204 | [http://blog.sungardas.com/CTOLabs/](http://blog.sungardas.com/CTOLabs/) 205 | 206 | 207 | [labs-github-url]: https://sungardas.github.io 208 | [labs-logo]: https://raw.githubusercontent.com/SungardAS/repo-assets/master/images/logos/sungardas-labs-logo-small.png 209 | [npm-image]: https://badge.fury.io/js/lambda-formation.svg 210 | [npm-url]: https://npmjs.org/package/lambda-formation 211 | [travis-image]: 212 | https://travis-ci.org/SungardAS/lambda-formation.svg?branch=master 213 | [travis-url]: https://travis-ci.org/SungardAS/lambda-formation 214 | [daviddm-image]: 215 | https://david-dm.org/SungardAS/lambda-formation.svg?theme=shields.io 216 | [daviddm-url]: https://david-dm.org/SungardAS/lambda-formation 217 | [coveralls-image]: 218 | https://coveralls.io/repos/SungardAS/lambda-formation/badge.svg 219 | [coveralls-url]: 220 | https://coveralls.io/r/SungardAS/lambda-formation 221 | [codeclimate-image]: https://codeclimate.com/github/SungardAS/lambda-formation/badges/gpa.svg 222 | [codeclimate-url]: https://codeclimate.com/github/SungardAS/lambda-formation 223 | 224 | [aws-lambda-url]: https://aws.amazon.com/lambda/ 225 | [aws-cloudformation-url]: https://aws.amazon.com/cloudformation/ 226 | [aws-custom-resources-url]: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html 227 | [lambda-formation-image]: ./docs/images/lambda-formation.png?raw=true 228 | [lambda-formation-url]: https://github.com/SungardAS/lambda-formation 229 | 230 | [winston-github-url]: https://github.com/winstonjs/winston -------------------------------------------------------------------------------- /docs/images/lambda-formation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SungardAS/lambda-formation/2b92bb0cf853c5d06f1c810a39687cab0668610e/docs/images/lambda-formation.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var util = require('./lib/util'); 2 | var resource = require('./lib/resource'); 3 | var project = require('./lib/project'); 4 | var logger = require('./lib/logger'); 5 | 6 | module.exports.util = util; 7 | module.exports.resource = resource; 8 | module.exports.project = project; 9 | module.exports.logger = logger; 10 | -------------------------------------------------------------------------------- /lib/logger/index.js: -------------------------------------------------------------------------------- 1 | var winston = require("winston"); 2 | 3 | module.exports = new (winston.Logger)({ 4 | level: process.env.CFN_LOG_LEVEL || 'info', 5 | transports: [ 6 | new (winston.transports.Console)({ 7 | json: true, 8 | stringify: true, 9 | timestamp: true 10 | }) 11 | ] 12 | }); 13 | -------------------------------------------------------------------------------- /lib/project/handler.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var _s = require("underscore.string"); 3 | var callerId = require("caller-id"); 4 | var fs = require("fs"); 5 | var path = require("path"); 6 | var util = require("../util"); 7 | var logger = require("../logger"); 8 | 9 | 10 | /** 11 | * Main entrypoint for a lambda-formation project 12 | * @param {Object} event - The event object 13 | * @param {string} event.ResourceType - The resource to target in the project 14 | * @param {string} event.RequestType - Create|Update|Delete for the resource 15 | * @param {Object} context - Context passed by Lambda 16 | * @returns {undefined} 17 | */ 18 | module.exports = function handler(event,context) { 19 | var ev; 20 | 21 | logger.debug('Lambda-formation project entrypoint', { event: event, context: context }); 22 | 23 | try { 24 | ev = util.normalizeEvent(event,context); 25 | } 26 | catch(e) { 27 | util.done(e,event,context); 28 | } 29 | 30 | var resourceType = ev ? (ev.resourceType || ev.ResourceType) : undefined; 31 | 32 | if (!resourceType) 33 | return util.done("resourceType|ResourceType must be defined in the event object", ev, context); 34 | 35 | if (_s.startsWith(resourceType, 'Custom', ev,context)) 36 | resourceType = _s.strRight(resourceType, 'Custom::'); 37 | 38 | var caller = callerId.getData(); 39 | var resourcePath = path.join(caller.filePath,'..','lib','resources'); 40 | 41 | if (!_.includes(getDirectories(resourcePath), resourceType)) 42 | return util.done("Invalid resourceType:" + resourceType, ev, context); 43 | 44 | try { 45 | require(path.join(resourcePath, resourceType)).handler(ev, context); 46 | } catch(e) { 47 | util.done(e,ev,context); 48 | } 49 | }; 50 | 51 | function getDirectories(srcpath) { 52 | return fs.readdirSync(srcpath).filter(function(file) { 53 | return fs.statSync(path.join(srcpath, file)).isDirectory(); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /lib/project/index.js: -------------------------------------------------------------------------------- 1 | var handler = require('./handler'); 2 | 3 | module.exports.handler = handler; 4 | -------------------------------------------------------------------------------- /lib/resource/create.js: -------------------------------------------------------------------------------- 1 | var util = require('../util'); 2 | 3 | module.exports = function(event,context,cb) { 4 | 5 | if (typeof cb !== 'function') { 6 | throw("cb must be function"); 7 | } 8 | else { 9 | return cb(null,event,context); 10 | } 11 | 12 | }; 13 | -------------------------------------------------------------------------------- /lib/resource/delete.js: -------------------------------------------------------------------------------- 1 | var util = require('../util'); 2 | 3 | module.exports = function(event,context,cb) { 4 | 5 | try { 6 | util.getPhysicalResourceId(event, context); 7 | } 8 | catch(e) { 9 | return util.done(e.message, event, context); 10 | } 11 | 12 | if (typeof cb !== 'function') { 13 | return cb("cb must be function"); 14 | } 15 | else { 16 | return cb(null,event,context); 17 | } 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /lib/resource/handler.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var callerId = require("caller-id"); 3 | var path = require("path"); 4 | var util = require("../util"); 5 | var logger = require("../logger"); 6 | 7 | /** 8 | * Entrypoint for a ResourceType 9 | * @param {Object} event - The event object 10 | * @param {string} event.RequestType - Create|Update|Delete for the resource 11 | * @param {Object} context - Context passed by Lambda 12 | * @returns {undefined} 13 | */ 14 | module.exports = function(event, context) { 15 | 16 | var ev; 17 | 18 | logger.debug('Lambda-formation resource entrypoint', { event: event, context: context }); 19 | 20 | try { 21 | if (event._normalized !== true) { 22 | ev = util.normalizeEvent(event,context); 23 | } 24 | else { 25 | ev = event; 26 | } 27 | 28 | var requestType = util.getRequestType(ev,context); 29 | } 30 | catch(e) { 31 | return util.done(e.message, ev, context); 32 | } 33 | 34 | if (!_.includes(['create','update','delete'],requestType)) 35 | return util.done("Invalid requestType: " + requestType,ev,context); 36 | 37 | var caller = callerId.getData(); 38 | var requirePath = path.join(caller.filePath,'..',requestType); 39 | require(requirePath).handler(ev,context); 40 | } 41 | -------------------------------------------------------------------------------- /lib/resource/index.js: -------------------------------------------------------------------------------- 1 | var create = require("./create"); 2 | var destroy = require("./delete"); 3 | var handler = require("./handler"); 4 | var update = require("./update"); 5 | 6 | module.exports = { 7 | create: create, 8 | delete: destroy, 9 | handler: handler, 10 | update: update 11 | }; 12 | -------------------------------------------------------------------------------- /lib/resource/update.js: -------------------------------------------------------------------------------- 1 | var util = require('../util'); 2 | 3 | module.exports = function(event,context,cb) { 4 | 5 | try { 6 | util.getPhysicalResourceId(event, context); 7 | } 8 | catch(e) { 9 | return util.done(e.message, event, context); 10 | } 11 | 12 | if (typeof cb !== 'function') { 13 | return cb("cb must be function"); 14 | } 15 | else { 16 | return cb(null,event,context); 17 | } 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /lib/util/done.js: -------------------------------------------------------------------------------- 1 | var getErrorString = require("./getErrorString"); 2 | var request = require("request"); 3 | var response = require("cfn-responder"); 4 | 5 | 6 | module.exports = function(err, event, context, obj, physicalResourceId, options) { 7 | options = options || {}; 8 | 9 | /* 10 | * Is this request from CloudFormation 11 | */ 12 | if (event && event.StackId) { 13 | /* 14 | * If CloudFormation sent a physicalReourceId and none has been passed here, use what CloudFormation sent. 15 | * this will ensure that updates and deletes will continute to be correlated in CloudFormation 16 | */ 17 | physicalResourceId = physicalResourceId || event.PhysicalResourceId; 18 | 19 | var responseStatus = response.SUCCESS; 20 | if (err) { 21 | event.Reason = getErrorString(err); 22 | responseStatus = response.FAILED; 23 | } 24 | response.send(event, context, responseStatus, obj, physicalResourceId, options.cfn_responder); 25 | } 26 | else { 27 | context.done(err,obj,physicalResourceId); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /lib/util/getErrorString.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | module.exports = function getErrorString(err) { 4 | if (_.isString(err)) return err; 5 | 6 | if (_.isObject(err) && err.message) return err.message; 7 | 8 | return JSON.stringify(err); 9 | }; 10 | -------------------------------------------------------------------------------- /lib/util/getPhysicalResourceId.js: -------------------------------------------------------------------------------- 1 | var done = require('./done'), 2 | _ = require('lodash'); 3 | 4 | var PhysicalResourceIdNotFound = function() { 5 | this.message = "physicalResourceId|physicalResourceId|id must be defined"; 6 | this.name = "PhysicalResourceIdNotFound"; 7 | }; 8 | 9 | module.exports = function(event,context) { 10 | 11 | var id = event.PhysicalResourceId || event.physicalResourceId || event.id; 12 | 13 | if (!id) { 14 | throw(new PhysicalResourceIdNotFound()); 15 | } 16 | 17 | return id; 18 | }; 19 | -------------------------------------------------------------------------------- /lib/util/getRequestType.js: -------------------------------------------------------------------------------- 1 | var done = require('./done'), 2 | _ = require('lodash'); 3 | 4 | var RequestTypeNotFound = function() { 5 | this.message = "requestType not defined it must be set to create|update|delete"; 6 | this.name = "RequestTypeNotFound"; 7 | }; 8 | 9 | module.exports = function(event,context) { 10 | 11 | var requestType = event.RequestType || event.requestType; 12 | 13 | if (!requestType) { 14 | throw(new RequestTypeNotFound()); 15 | } 16 | 17 | requestType = requestType.toLowerCase(); 18 | 19 | return requestType; 20 | }; 21 | -------------------------------------------------------------------------------- /lib/util/index.js: -------------------------------------------------------------------------------- 1 | var done = require("./done"); 2 | var getPhysicalResourceId = require("./getPhysicalResourceId"); 3 | var getErrorString = require("./getErrorString"); 4 | var getRequestType = require("./getRequestType"); 5 | var normalizeEvent = require("./normalizeEvent"); 6 | 7 | module.exports = { 8 | done: done, 9 | getPhysicalResourceId: getPhysicalResourceId, 10 | getErrorString: getErrorString, 11 | getRequestType: getRequestType, 12 | normalizeEvent: normalizeEvent 13 | } 14 | -------------------------------------------------------------------------------- /lib/util/normalizeEvent.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | module.exports = function(event,context) { 4 | var ev; 5 | 6 | if ( 7 | event 8 | && event.Records 9 | && event.Records[0] 10 | && event.Records[0].EventSource === "aws:sns" 11 | ) { 12 | try { 13 | ev = JSON.parse(event.Records[0].Sns.Message); 14 | ev._originalInvoke = event; 15 | } 16 | catch(error) { 17 | throw "Detected SNS Message but payload not parsable JSON"; 18 | } 19 | } 20 | else { 21 | ev = event; 22 | } 23 | ev._normalized = true; 24 | return ev; 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-formation", 3 | "version": "0.1.11", 4 | "publishConfig": { 5 | "tag": "latest" 6 | }, 7 | "description": "A small framework for building nodejs AWS Lambda projects that are compatible with AWS CloudFormation Custom Resources", 8 | "main": "index.js", 9 | "scripts": { 10 | "test": "mocha test/unit/*.test.js test/integration/**/*.test.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/SungardAS/lambda-formation.git" 15 | }, 16 | "keywords": [ 17 | "lambda-formation" 18 | ], 19 | "author": "kevin.mcgrath@sungardas.com", 20 | "license": "Apache-2.0", 21 | "bugs": { 22 | "url": "https://github.com/SungardAS/lambda-formation/issues" 23 | }, 24 | "homepage": "https://github.com/SungardAS/lambda-formation#readme", 25 | "dependencies": { 26 | "caller-id": "^0.1.0", 27 | "cfn-responder": "^1.0.2", 28 | "lodash": "^4.17.4", 29 | "request": "^2.81.0", 30 | "underscore.string": "^3.3.4", 31 | "winston": "^2.3.1" 32 | }, 33 | "devDependencies": { 34 | "assert": "^1.4.1", 35 | "coveralls": "^2.13.1", 36 | "istanbul": "^0.4.5", 37 | "mocha": "^3.4.1", 38 | "nock": "^9.0.13", 39 | "vows": "^0.8.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/fixtures/project1/index.js: -------------------------------------------------------------------------------- 1 | var handler = require('../../../lib/project/handler'); 2 | 3 | module.exports.handler = function() { 4 | handler.apply(this,arguments); 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/project1/lib/resources/resource1/create.js: -------------------------------------------------------------------------------- 1 | var create = require('../../../../../../lib/resource/create') 2 | util = require('../../../../../../lib/util'); 3 | 4 | module.exports.handler = function(event,context) { 5 | create.apply(this,[event,context,myCreate]); 6 | }; 7 | 8 | var myCreate = function(err,event,context) { 9 | if (err) 10 | return util.done(err); 11 | 12 | util.done(null,event,context,{id: 1, name: "First Resource"},1); 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/project1/lib/resources/resource1/delete.js: -------------------------------------------------------------------------------- 1 | var destroy = require('../../../../../../lib/resource/delete') 2 | util = require('../../../../../../lib/util'); 3 | 4 | module.exports.handler = function(event,context) { 5 | destroy.apply(this,[event,context,myDelete]); 6 | }; 7 | 8 | var myDelete = function(err,event,context) { 9 | if (err) 10 | return util.done(err); 11 | 12 | util.done(null,event,context); 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/project1/lib/resources/resource1/index.js: -------------------------------------------------------------------------------- 1 | var handler = require('../../../../../../lib/resource/handler'); 2 | 3 | module.exports.handler = function() { 4 | handler.apply(this,arguments); 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/project1/lib/resources/resource1/update.js: -------------------------------------------------------------------------------- 1 | var update = require('../../../../../../lib/resource/update') 2 | util = require('../../../../../../lib/util'); 3 | 4 | module.exports.handler = function(event,context) { 5 | update.apply(this,[event,context,myUpdate]); 6 | }; 7 | 8 | var myUpdate = function(err,event,context) { 9 | if (err) 10 | return util.done(err); 11 | 12 | util.done(null,event,context); 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/project1/lib/resources/resource2/create.js: -------------------------------------------------------------------------------- 1 | var create = require('../../../../../../lib/resource/create') 2 | util = require('../../../../../../lib/util'); 3 | 4 | // Bad function, no callback 5 | module.exports.handler = function(event, context) { 6 | create.apply(this, [event, context]); 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/project1/lib/resources/resource2/delete.js: -------------------------------------------------------------------------------- 1 | var destroy = require('../../../../../../lib/resource/delete') 2 | util = require('../../../../../../lib/util'); 3 | 4 | // Bad function, no callback 5 | module.exports.handler = function(event, context) { 6 | destroy.apply(this, [event, context]); 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/project1/lib/resources/resource2/update.js: -------------------------------------------------------------------------------- 1 | var update = require('../../../../../../lib/resource/update') 2 | util = require('../../../../../../lib/util'); 3 | 4 | // Bad function, no callback 5 | module.exports.handler = function(event, context) { 6 | update.apply(this, [event, context]); 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/project1/lib/resources/resource3/create.js: -------------------------------------------------------------------------------- 1 | var create = require('../../../../../../lib/resource/create') 2 | util = require('../../../../../../lib/util'); 3 | 4 | module.exports.handler = function(event,context) { 5 | create.apply(this,[event,context,myCreate]); 6 | }; 7 | 8 | var myCreate = function(err,event,context) { 9 | if (err) 10 | return util.done(err); 11 | 12 | dont_fail_badly(); 13 | 14 | util.done(null,event,context,{id: 1, name: "First Resource"},1); 15 | }; 16 | -------------------------------------------------------------------------------- /test/fixtures/project1/lib/resources/resource3/delete.js: -------------------------------------------------------------------------------- 1 | var destroy = require('../../../../../../lib/resource/delete') 2 | util = require('../../../../../../lib/util'); 3 | 4 | // Bad function, no callback 5 | module.exports.handler = function(event, context) { 6 | destroy.apply(this, [event, context]); 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/project1/lib/resources/resource3/index.js: -------------------------------------------------------------------------------- 1 | var handler = require('../../../../../../lib/resource/handler'); 2 | 3 | module.exports.handler = function() { 4 | handler.apply(this,arguments); 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/project1/lib/resources/resource3/update.js: -------------------------------------------------------------------------------- 1 | var update = require('../../../../../../lib/resource/update') 2 | util = require('../../../../../../lib/util'); 3 | 4 | // Bad function, no callback 5 | module.exports.handler = function(event, context) { 6 | update.apply(this, [event, context]); 7 | }; 8 | -------------------------------------------------------------------------------- /test/integration/project1/project.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var project1 = require('../../fixtures/project1'); 3 | 4 | describe('project1', function() { 5 | describe("handler", function() { 6 | 7 | it("should fail if event object is empty", function(cb) { 8 | var context = { 9 | done: function(err) { 10 | assert(err); 11 | cb(); 12 | } 13 | }; 14 | project1.handler({},context); 15 | }); 16 | 17 | it("should fail without requestType", function() { 18 | var event = { 19 | resourceType: "resource1" 20 | }; 21 | 22 | var context = { 23 | done: function(err) { 24 | assert(err); 25 | } 26 | }; 27 | project1.handler(event,context); 28 | }); 29 | 30 | it("should fail for a bad requestType", function() { 31 | var event = { 32 | resourceType: "resource1", 33 | requestType: "bogus" 34 | }; 35 | 36 | var context = { 37 | done: function(err) { 38 | assert(err); 39 | } 40 | }; 41 | project1.handler(event,context); 42 | }); 43 | 44 | it("should fail for a bad resourceType", function() { 45 | var event = { 46 | resourceType: "resourceBADBADBAD", 47 | requestType: "create" 48 | }; 49 | 50 | var context = { 51 | done: function(err) { 52 | assert(err); 53 | } 54 | }; 55 | project1.handler(event,context); 56 | }); 57 | 58 | it("should handle a Custom Resource from CloudFormation", function() { 59 | var event = { 60 | ResourceType: "Custom::resource1", 61 | RequestType: "Create" 62 | }; 63 | 64 | var context = { 65 | done: function(err) { 66 | assert.ifError(err); 67 | } 68 | }; 69 | project1.handler(event,context); 70 | }); 71 | 72 | it("should handle a Custom Resource from SNS", function() { 73 | var event = { 74 | Records: [{ 75 | EventSource: "aws:sns", 76 | Sns: { 77 | Message: JSON.stringify({ 78 | ResourceType: "Custom::resource1", 79 | RequestType: "Create" 80 | }) 81 | } 82 | }] 83 | }; 84 | 85 | var context = { 86 | done: function(err) { 87 | assert.ifError(err); 88 | } 89 | }; 90 | project1.handler(event,context); 91 | }); 92 | 93 | it("should fail if SNS message is not valid JSON", function() { 94 | var event = { 95 | Records: [{ 96 | EventSource: "aws:sns", 97 | Sns: { 98 | Message: "[" 99 | } 100 | }] 101 | }; 102 | 103 | var context = { 104 | done: function(err) { 105 | assert(err); 106 | } 107 | }; 108 | project1.handler(event,context); 109 | }); 110 | 111 | describe("resource1", function() { 112 | 113 | describe("create", function() { 114 | 115 | it("should run without error", function(cb) { 116 | var event = { 117 | resourceType: "resource1", 118 | requestType: "create" 119 | }; 120 | 121 | var context = { 122 | done: function(err,data,id) { 123 | assert.ifError(err); 124 | assert(id,"Create must return a unique ID"); 125 | cb(); 126 | } 127 | }; 128 | project1.handler(event,context); 129 | }); 130 | }); 131 | 132 | describe("update", function() { 133 | 134 | it("should fail if no PhysicalResourceId is provided", function(cb) { 135 | var event = { 136 | resourceType: "resource1", 137 | requestType: "update" 138 | }; 139 | 140 | var context = { 141 | done: function(err) { 142 | assert(err); 143 | cb(); 144 | } 145 | }; 146 | project1.handler(event,context); 147 | }); 148 | 149 | it("should run if PhysicalResourceId (PhysicalResourceId) is provided", function(cb) { 150 | var event = { 151 | resourceType: "resource1", 152 | requestType: "update", 153 | PhysicalResourceId: 1 154 | }; 155 | 156 | var context = { 157 | done: function(err) { 158 | assert.ifError(err); 159 | cb(); 160 | } 161 | }; 162 | project1.handler(event,context); 163 | }); 164 | 165 | it("should run if PhysicalResourceId (physicalResourceId) is provided", function(cb) { 166 | var event = { 167 | resourceType: "resource1", 168 | requestType: "update", 169 | physicalResourceId: 1 170 | }; 171 | 172 | var context = { 173 | done: function(err) { 174 | assert.ifError(err); 175 | cb(); 176 | } 177 | }; 178 | project1.handler(event,context); 179 | }); 180 | 181 | it("should run if PhysicalResourceId (id) is provided", function(cb) { 182 | var event = { 183 | resourceType: "resource1", 184 | requestType: "update", 185 | id: 1 186 | }; 187 | 188 | var context = { 189 | done: function(err) { 190 | assert.ifError(err); 191 | cb(); 192 | } 193 | }; 194 | project1.handler(event,context); 195 | }); 196 | }); 197 | 198 | 199 | describe("delete", function() { 200 | it("should fail if no PhysicalResourceId is provided", function(cb) { 201 | var event = { 202 | resourceType: "resource1", 203 | requestType: "delete" 204 | }; 205 | 206 | var context = { 207 | done: function(err) { 208 | assert(err); 209 | cb(); 210 | } 211 | }; 212 | project1.handler(event,context); 213 | }); 214 | 215 | it("should run if PhysicalResourceId (PhysicalResourceId) is provided", function(cb) { 216 | var event = { 217 | resourceType: "resource1", 218 | requestType: "delete", 219 | PhysicalResourceId: 1 220 | }; 221 | 222 | var context = { 223 | done: function(err) { 224 | assert.ifError(err); 225 | cb(); 226 | } 227 | }; 228 | project1.handler(event,context); 229 | }); 230 | 231 | it("should run if PhysicalResourceId (physicalResourceId) is provided", function(cb) { 232 | var event = { 233 | resourceType: "resource1", 234 | requestType: "delete", 235 | physicalResourceId: 1 236 | }; 237 | 238 | var context = { 239 | done: function(err) { 240 | assert.ifError(err); 241 | cb(); 242 | } 243 | }; 244 | project1.handler(event,context); 245 | }); 246 | 247 | it("should run if PhysicalResourceId (id) is provided", function(cb) { 248 | var event = { 249 | resourceType: "resource1", 250 | requestType: "delete", 251 | id: 1 252 | }; 253 | 254 | var context = { 255 | done: function(err) { 256 | assert.ifError(err); 257 | cb(); 258 | } 259 | }; 260 | project1.handler(event,context); 261 | }); 262 | }); 263 | }); 264 | 265 | describe("resource3", function() { 266 | 267 | describe("create", function() { 268 | 269 | it("should run without error", function(cb) { 270 | var event = { 271 | resourceType: "resource3", 272 | requestType: "create" 273 | }; 274 | 275 | var context = { 276 | done: function(err,data,id) { 277 | assert.equal(err, 'ReferenceError: dont_fail_badly is not defined'); 278 | cb(); 279 | } 280 | }; 281 | project1.handler(event,context); 282 | }); 283 | }); 284 | 285 | }); 286 | }); 287 | }); 288 | -------------------------------------------------------------------------------- /test/integration/project1/resource1.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | resource1 = require('../../fixtures/project1/lib/resources/resource1'); 3 | 4 | describe('project1', function() { 5 | describe("resource1 handler", function() { 6 | 7 | 8 | it("should fail without requestType", function() { 9 | var event = {}; 10 | 11 | var context = { 12 | done: function(err) { 13 | assert(err); 14 | } 15 | }; 16 | resource1.handler(event,context); 17 | }); 18 | 19 | 20 | describe("create", function() { 21 | 22 | it("should run without error", function(cb) { 23 | var event = { 24 | requestType: "create" 25 | }; 26 | 27 | var context = { 28 | done: function(err,data,id) { 29 | assert.ifError(err); 30 | assert(id,"Create must return a unique ID"); 31 | cb(); 32 | } 33 | }; 34 | resource1.handler(event,context); 35 | }); 36 | }); 37 | 38 | describe("update", function() { 39 | 40 | it("should fail if no PhysicalResourceId is provided", function(cb) { 41 | var event = { 42 | requestType: "update" 43 | }; 44 | 45 | var context = { 46 | done: function(err) { 47 | assert(err); 48 | cb(); 49 | } 50 | }; 51 | resource1.handler(event,context); 52 | }); 53 | 54 | it("should run if PhysicalResourceId (PhysicalResourceId) is provided", function(cb) { 55 | var event = { 56 | requestType: "update", 57 | PhysicalResourceId: 1 58 | }; 59 | 60 | var context = { 61 | done: function(err) { 62 | assert.ifError(err); 63 | cb(); 64 | } 65 | }; 66 | resource1.handler(event,context); 67 | }); 68 | 69 | it("should run if PhysicalResourceId (physicalResourceId) is provided", function(cb) { 70 | var event = { 71 | requestType: "update", 72 | physicalResourceId: 1 73 | }; 74 | 75 | var context = { 76 | done: function(err) { 77 | assert.ifError(err); 78 | cb(); 79 | } 80 | }; 81 | resource1.handler(event,context); 82 | }); 83 | 84 | it("should run if PhysicalResourceId (id) is provided", function(cb) { 85 | var event = { 86 | requestType: "update", 87 | id: 1 88 | }; 89 | 90 | var context = { 91 | done: function(err) { 92 | assert.ifError(err); 93 | cb(); 94 | } 95 | }; 96 | resource1.handler(event,context); 97 | }); 98 | }); 99 | 100 | 101 | describe("delete", function() { 102 | it("should fail if no PhysicalResourceId is provided", function(cb) { 103 | var event = { 104 | requestType: "delete" 105 | }; 106 | 107 | var context = { 108 | done: function(err) { 109 | assert(err); 110 | cb(); 111 | } 112 | }; 113 | resource1.handler(event,context); 114 | }); 115 | 116 | it("should run if PhysicalResourceId (PhysicalResourceId) is provided", function(cb) { 117 | var event = { 118 | requestType: "delete", 119 | PhysicalResourceId: 1 120 | }; 121 | 122 | var context = { 123 | done: function(err) { 124 | assert.ifError(err); 125 | cb(); 126 | } 127 | }; 128 | resource1.handler(event,context); 129 | }); 130 | 131 | it("should run if PhysicalResourceId (physicalResourceId) is provided", function(cb) { 132 | var event = { 133 | requestType: "delete", 134 | physicalResourceId: 1 135 | }; 136 | 137 | var context = { 138 | done: function(err) { 139 | assert.ifError(err); 140 | cb(); 141 | } 142 | }; 143 | resource1.handler(event,context); 144 | }); 145 | 146 | it("should run if PhysicalResourceId (id) is provided", function(cb) { 147 | var event = { 148 | requestType: "delete", 149 | id: 1 150 | }; 151 | 152 | var context = { 153 | done: function(err) { 154 | assert.ifError(err); 155 | cb(); 156 | } 157 | }; 158 | resource1.handler(event,context); 159 | }); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /test/integration/project1/resource1_create.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | create = require('../../fixtures/project1/lib/resources/resource1/create'); 3 | 4 | describe('project1', function() { 5 | describe("resource1", function() { 6 | 7 | describe("create", function() { 8 | 9 | it("should run without error", function(cb) { 10 | var event = {}; 11 | 12 | var context = { 13 | done: function(err,data,id) { 14 | assert.ifError(err); 15 | assert(id,"Create must return a unique ID"); 16 | cb(); 17 | } 18 | }; 19 | create.handler(event,context); 20 | }); 21 | }); 22 | 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/integration/project1/resource1_delete.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | destroy = require('../../fixtures/project1/lib/resources/resource1/delete'); 3 | 4 | describe('project1', function() { 5 | describe("resource1", function() { 6 | 7 | describe("delete", function() { 8 | it("should fail if no PhysicalResourceId is provided", function(cb) { 9 | var event = {}; 10 | 11 | var context = { 12 | done: function(err) { 13 | assert(err); 14 | cb(); 15 | } 16 | }; 17 | destroy.handler(event,context); 18 | }); 19 | 20 | it("should run if PhysicalResourceId (PhysicalResourceId) is provided", function(cb) { 21 | var event = { 22 | PhysicalResourceId: 1 23 | }; 24 | 25 | var context = { 26 | done: function(err) { 27 | assert.ifError(err); 28 | cb(); 29 | } 30 | }; 31 | destroy.handler(event,context); 32 | }); 33 | 34 | it("should run if PhysicalResourceId (physicalResourceId) is provided", function(cb) { 35 | var event = { 36 | physicalResourceId: 1 37 | }; 38 | 39 | var context = { 40 | done: function(err) { 41 | assert.ifError(err); 42 | cb(); 43 | } 44 | }; 45 | destroy.handler(event,context); 46 | }); 47 | 48 | it("should run if PhysicalResourceId (id) is provided", function(cb) { 49 | var event = { 50 | id: 1 51 | }; 52 | 53 | var context = { 54 | done: function(err) { 55 | assert.ifError(err); 56 | cb(); 57 | } 58 | }; 59 | destroy.handler(event,context); 60 | }); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/integration/project1/resource1_update.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | update = require('../../fixtures/project1/lib/resources/resource1/update'); 3 | 4 | describe('project1', function() { 5 | describe("resource1", function() { 6 | 7 | 8 | describe("update", function() { 9 | 10 | it("should fail if no PhysicalResourceId is provided", function(cb) { 11 | var event = {}; 12 | 13 | var context = { 14 | done: function(err) { 15 | assert(err); 16 | cb(); 17 | } 18 | }; 19 | update.handler(event,context); 20 | }); 21 | 22 | it("should run if PhysicalResourceId (PhysicalResourceId) is provided", function(cb) { 23 | var event = { 24 | PhysicalResourceId: 1 25 | }; 26 | 27 | var context = { 28 | done: function(err) { 29 | assert.ifError(err); 30 | cb(); 31 | } 32 | }; 33 | update.handler(event,context); 34 | }); 35 | 36 | it("should run if PhysicalResourceId (physicalResourceId) is provided", function(cb) { 37 | var event = { 38 | physicalResourceId: 1 39 | }; 40 | 41 | var context = { 42 | done: function(err) { 43 | assert.ifError(err); 44 | cb(); 45 | } 46 | }; 47 | update.handler(event,context); 48 | }); 49 | 50 | it("should run if PhysicalResourceId (id) is provided", function(cb) { 51 | var event = { 52 | id: 1 53 | }; 54 | 55 | var context = { 56 | done: function(err) { 57 | assert.ifError(err); 58 | cb(); 59 | } 60 | }; 61 | update.handler(event,context); 62 | }); 63 | }); 64 | 65 | 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/integration/project1/resource2_create.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | create = require('../../fixtures/project1/lib/resources/resource2/create'); 3 | 4 | describe('project1', function() { 5 | describe("resource2", function() { 6 | 7 | describe("create", function() { 8 | 9 | it("should fail", function() { 10 | var event = {}; 11 | 12 | var context = { 13 | done: function(err) { 14 | } 15 | }; 16 | assert.throws( 17 | function() { 18 | create.handler(event,context); 19 | } 20 | ); 21 | }); 22 | }); 23 | 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/integration/project1/resource2_delete.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | destroy = require('../../fixtures/project1/lib/resources/resource2/delete'); 3 | 4 | describe('project1', function() { 5 | describe("resource2", function() { 6 | 7 | describe("delete", function() { 8 | 9 | it("should fail", function() { 10 | var event = { 11 | physicalResourceId: 1 12 | }; 13 | 14 | var context = { 15 | done: function(err) { 16 | } 17 | }; 18 | assert.throws( 19 | function() { 20 | destroy.handler(event,context); 21 | } 22 | ); 23 | }); 24 | }); 25 | 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/integration/project1/resource2_update.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | update = require('../../fixtures/project1/lib/resources/resource2/update'); 3 | 4 | describe('project1', function() { 5 | describe("resource2", function() { 6 | 7 | describe("update", function() { 8 | 9 | it("should fail", function() { 10 | var event = { 11 | physicalResourceId: 1 12 | }; 13 | 14 | var context = { 15 | done: function(err) { 16 | } 17 | }; 18 | assert.throws( 19 | function() { 20 | update.handler(event,context); 21 | } 22 | ); 23 | }); 24 | }); 25 | 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/unit/done.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var nock = require('nock'); 3 | var util = require('../../lib/util'); 4 | 5 | 6 | describe("util done", function() { 7 | 8 | before(function() { 9 | nock('https://fake.url') 10 | .put('/', {"Status":"SUCCESS","Reason":"See the details in CloudWatch Log Stream: undefined","StackId":"arn:aws:cloudformation:us-east-1:namespace:stack/stack-name/guid","RequestId":"unique id for this create request","LogicalResourceId":"name of resource in template","Data":{}}) 11 | .reply(200, {}); 12 | 13 | nock('https://fake.url') 14 | .put('/', {"Status":"FAILED","Reason":"This didn't work","StackId":"arn:aws:cloudformation:us-east-1:namespace:stack/stack-name/guid","RequestId":"unique id for this create request","LogicalResourceId":"name of resource in template","Data":{}}) 15 | .reply(200, {}); 16 | }); 17 | 18 | 19 | it("should call context if lambda", function(done) { 20 | var context = { 21 | done: function(err,obj) { 22 | assert(!obj.StackId); 23 | done(); 24 | } 25 | }; 26 | 27 | util.done(null,{},context,{}); 28 | }); 29 | 30 | it("should call cfn-responder if CloudFormation", function(done) { 31 | var context = { 32 | done: function(err,obj) { 33 | assert(obj.StackId); 34 | done(); 35 | } 36 | }; 37 | 38 | util.done( 39 | null, 40 | { 41 | RequestType: "Create", 42 | RequestId: "unique id for this create request", 43 | ResponseURL: "https://fake.url", 44 | ResourceType: "Custom::MyCustomResourceType", 45 | LogicalResourceId: "name of resource in template", 46 | StackId: "arn:aws:cloudformation:us-east-1:namespace:stack/stack-name/guid" 47 | }, 48 | context,{} 49 | ); 50 | }); 51 | 52 | it("should set cfn-responder to FAILED for CloudFormation if err", function(done) { 53 | var context = { 54 | done: function(err,obj) { 55 | assert(obj.StackId); 56 | done(); 57 | } 58 | }; 59 | 60 | util.done( 61 | "This didn't work", 62 | { 63 | RequestType: "Create", 64 | RequestId: "unique id for this create request", 65 | ResponseURL: "https://fake.url", 66 | ResourceType: "Custom::MyCustomResourceType", 67 | LogicalResourceId: "name of resource in template", 68 | StackId: "arn:aws:cloudformation:us-east-1:namespace:stack/stack-name/guid" 69 | }, 70 | context,{} 71 | ); 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /test/unit/getErrorString.test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var util = require("../../lib/util"); 3 | 4 | describe("util getErrorString", function() { 5 | 6 | it("should work with a string", function() { 7 | var err = "this is an error"; 8 | var es = util.getErrorString(err); 9 | assert.equal(es,"this is an error"); 10 | }); 11 | 12 | it("should work with an Error object", function() { 13 | var err = new Error("this is an error"); 14 | var es = util.getErrorString(err); 15 | assert.equal(es,"this is an error"); 16 | }); 17 | 18 | it("should work with any object", function() { 19 | var err = {"msg": "some custom error"}; 20 | var es = util.getErrorString(err); 21 | assert.equal(es,'{"msg":"some custom error"}'); 22 | }); 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /test/unit/load.test.js: -------------------------------------------------------------------------------- 1 | var lambdaFormation = require('../../'); 2 | var assert = require('assert'); 3 | 4 | describe("util.load", function() { 5 | it("should load", function() { 6 | assert(lambdaFormation); 7 | }); 8 | 9 | it("should have util", function() { 10 | assert(lambdaFormation.util); 11 | }); 12 | 13 | it("should have util.done", function() { 14 | assert(lambdaFormation.util.done); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/unit/logger.test.js: -------------------------------------------------------------------------------- 1 | var lf = require('../../'); 2 | var winston = require('winston'); 3 | var vows = require('vows'), 4 | assert = require('assert'); 5 | 6 | describe("logger", function () { 7 | before(function() { 8 | lf.logger.clear(); 9 | lf.logger.add(winston.transports.Memory); 10 | }); 11 | 12 | it("should exist in lambda-formation", function () { 13 | assert.isObject(lf.logger); 14 | assert.isFunction(lf.logger.log); 15 | }); 16 | 17 | it("should log only info messages by default", function () { 18 | lf.logger.log('debug', 'foobar1'); 19 | lf.logger.log('debug', 'foobar2'); 20 | 21 | lf.logger.log('info', 'foobar1'); 22 | lf.logger.log('info', 'foobar2'); 23 | 24 | assert.include(lf.logger.transports['memory'].writeOutput, 'info: foobar1'); 25 | assert.notInclude(lf.logger.transports['memory'].writeOutput, 'debug: foobar1'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/unit/normalizeEvent.test.js: -------------------------------------------------------------------------------- 1 | var normalizeEvent = require("../../lib/util/normalizeEvent"); 2 | var assert = require("assert"); 3 | 4 | describe("util.normalizeEvent", function() { 5 | if("should discover a SNS message and return message as the event object") { 6 | var invokeEvent = { 7 | Records: [{ 8 | EventSource: "aws:sns", 9 | Sns: { 10 | Message: JSON.stringify({ 11 | RequestType: "Create", 12 | ResourceType: "resource1" 13 | }) 14 | } 15 | }] 16 | }; 17 | 18 | var ev = normalizeEvent(invokeEvent); 19 | assert.equal(ev.RequestType,"Create"); 20 | assert.equal(ev.ResourceType,"resource1"); 21 | assert(ev._originalInvoke); 22 | assert(ev._normalized); 23 | } 24 | }); 25 | --------------------------------------------------------------------------------