├── .gitignore ├── LICENSE ├── README.md ├── dist ├── examples │ ├── basic-api.json │ ├── firehose.json │ ├── full-app.json │ ├── iot.json │ ├── s3-processing.json │ └── stream-test.json ├── img │ ├── aws │ │ ├── AWS-Lambda_Function.png │ │ ├── AWS-Step-Functions.png │ │ ├── Amazon-API-Gateway.png │ │ ├── Amazon-CloudWatch_Event-Time-Based.png │ │ ├── Amazon-Cognito.png │ │ ├── Amazon-DynamoDB_Table.png │ │ ├── Amazon-Kinesis-Data-Analytics.png │ │ ├── Amazon-Kinesis-Data-Firehose.png │ │ ├── Amazon-Kinesis-Data-Streams.png │ │ ├── Amazon-S3_Bucket.png │ │ ├── Amazon-SNS_Topic.png │ │ └── IoT_Rule.png │ └── logo.png └── index.html ├── package-lock.json ├── package.json ├── src ├── engines │ ├── sam.js │ └── servfrmwk.js └── index.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | node_modules/ 4 | dist/main.* 5 | dist/img/vis/* 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Danilo Poccia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless By Design 2 | 3 | ![Serverless by Design screenshot](https://danilop.s3.amazonaws.com/Images/serverless-by-design.png) 4 | 5 | Serverless By Design is a visual approach to serverless development: 6 | 7 | - An application is a network of _nodes_ (serverless resources, such as Lambda functions or S3 buckets) connected by _edges_ (their relationships, for example a trigger or a data flow) 8 | - _Edit_ an application adding nodes and edges following an _event-driven_ design 9 | - _Import_ a previously exported application to continue working on it 10 | - Choose a _runtime_, and _build_ your application (for example, using AWS SAM) 11 | - Optionally use _canary_ or _linear_ deployments for your future updates 12 | - Edit _templates_ and code files for the final configurations before deploying the application 13 | - _Export_ an application to save it for later use in a JSON file 14 | - Take a _picture_ of the application architecture to have a visual representation to share 15 | - Fine tune the _physics_ used to place nodes and edges on the screen, for example enable/disable it or choose another solver 16 | 17 | Serverless By Design runs in the browser and doesn't need an internet connection when installed locally. 18 | 19 | A live version is available at: http://sbd.danilop.net 20 | 21 | Think. Build. Repeat. 22 | 23 | ## License 24 | 25 | Copyright (c) 2017 Danilo Poccia, http://danilop.net 26 | 27 | This code is licensed under the The MIT License (MIT). Please see the LICENSE file that accompanies this project for the terms of use. 28 | 29 | 30 | ## Installation 31 | 32 | You need `node` and `npm`. Just run: 33 | 34 | ``` 35 | npm run build 36 | ``` 37 | 38 | to build it for production, then open `dist/index.html` with your favourite browser. 39 | 40 | For a development build, that you can debug with a browser, use: 41 | 42 | ``` 43 | npm run dev 44 | ``` 45 | 46 | 47 | ## Usage 48 | 49 | Here are a few examples to help you start: 50 | 51 | - [Basic API](https://sbd.danilop.net/?import=examples/basic-api.json) 52 | - [S3 Processing](https://sbd.danilop.net/?import=examples/s3-processing.json) 53 | - [Firehose Processing API](https://sbd.danilop.net/?import=examples/firehose.json) 54 | - [Streaming Analytics](https://sbd.danilop.net/?import=examples/stream-test.json) 55 | - [Some IoT](https://sbd.danilop.net/?import=examples/iot.json) 56 | - [All Together Now](https://sbd.danilop.net/?import=examples/full-app.json) 57 | 58 | 59 | ## Dependencies 60 | 61 | This code depends on: 62 | - [Vis.js](http://visjs.org) 63 | - [js-yaml](http://nodeca.github.io/js-yaml/) 64 | - [FileSaver.js](https://github.com/eligrey/FileSaver.js/) 65 | - [jszip](https://stuk.github.io/jszip/) 66 | - [font-awesome](http://fontawesome.io) 67 | - [JQuery](https://jquery.com) 68 | -------------------------------------------------------------------------------- /dist/examples/basic-api.json: -------------------------------------------------------------------------------- 1 | { "nodes": [{ "id": "MyApi", "x": -472, "y": -172.5, "label": "API Gateway\nMyApi", "title": "My REST API", "model": { "type": "api", "description": "My REST API" }, "group": "api", "shadow": false }, { "id": "MyBackEnd", "x": -271, "y": -158.5, "label": "Lambda Function\nMyBackEnd", "title": "My REST Back End", "model": { "type": "fn", "description": "My REST Back End" }, "group": "fn", "shadow": false }, { "id": "MyTable", "x": -85, "y": -149.5, "label": "DynamoDB Table\nMyTable", "title": "My Database Table", "model": { "type": "table", "description": "My Database Table" }, "group": "table", "shadow": false }], "edges": [{ "from": "MyApi", "to": "MyBackEnd", "label": "integration", "color": { "color": "blue" }, "id": "96ce4c49-5a81-4a25-9b21-2e7745076036" }, { "from": "MyBackEnd", "to": "MyTable", "label": "read/write", "color": { "color": "green" }, "dashes": true, "id": "a6d31a48-50f6-421b-9411-a8bf582d1887" }] } 2 | -------------------------------------------------------------------------------- /dist/examples/firehose.json: -------------------------------------------------------------------------------- 1 | {"nodes":[{"id":"MyFirehose","x":-273.8228840125392,"y":-66.99921630094045,"label":"Kinesis Firehose\nMyFirehose","model":{"type":"deliveryStream","description":""},"group":"deliveryStream","shadow":false},{"id":"MyBucket","x":-85.44827586206897,"y":-78.00391849529781,"label":"S3 Bucket\nMyBucket","model":{"type":"bucket","description":""},"group":"bucket","shadow":false},{"id":"TransformFirehose","x":-203.26332288401255,"y":68.94122257053291,"label":"Lambda Function\nTransformFirehose","model":{"type":"fn","description":""},"group":"fn","shadow":false},{"id":"ProcessEvents","x":-308.7789968652038,"y":-24.27507836990596,"label":"Lambda Function\nProcessEvents","model":{"type":"fn","description":""},"group":"fn","shadow":false},{"id":"MyAPI","x":-495.2115987460815,"y":-83.18260188087775,"label":"API Gateway\nMyAPI","model":{"type":"api","description":""},"group":"api","shadow":false},{"id":"MyIdentity","x":-472.5548589341693,"y":62.467868338558,"label":"Cognito Identity\nMyIdentity","model":{"type":"cognitoIdentity","description":""},"group":"cognitoIdentity","shadow":false}],"edges":[{"from":"MyFirehose","to":"MyBucket","label":"destination","color":{"color":"blue"},"id":"7abe84ac-808b-4d83-a4b8-c0c130f5bd4d"},{"from":"MyFirehose","to":"TransformFirehose","label":"transform","color":{"color":"blue"},"id":"b3d76546-e9f6-4bb4-9714-758330b7282b"},{"from":"ProcessEvents","to":"MyFirehose","label":"put","color":{"color":"green"},"dashes":true,"id":"ec684344-73c9-4671-8370-9e478fc83de1"},{"from":"MyAPI","to":"ProcessEvents","label":"integration","color":{"color":"blue"},"id":"b917a548-e642-4c1b-a506-7ea052e4965f"},{"from":"MyIdentity","to":"MyAPI","label":"authorize","color":{"color":"blue"},"id":"a13a1d54-6781-40c5-91f5-4a16794e6d1f"}]} -------------------------------------------------------------------------------- /dist/examples/full-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "id": "MyIdentity", 5 | "x": -374.203125, 6 | "y": -112, 7 | "label": "Cognito Identity\nMyIdentity", 8 | "title": "My Identity", 9 | "model": { 10 | "type": "cognitoIdentity", 11 | "description": "My Identity" 12 | }, 13 | "group": "cognitoIdentity", 14 | "shadow": false 15 | }, 16 | { 17 | "id": "MyApi", 18 | "x": -316.203125, 19 | "y": 39, 20 | "label": "API Gateway\nMyApi", 21 | "title": "My REST API", 22 | "model": { 23 | "type": "api", 24 | "description": "My REST API" 25 | }, 26 | "group": "api", 27 | "shadow": false 28 | }, 29 | { 30 | "id": "MyBackEnd", 31 | "x": -153.203125, 32 | "y": 54, 33 | "label": "Lambda Function\nMyBackEnd", 34 | "title": "My Back End Function", 35 | "model": { 36 | "type": "fn", 37 | "description": "My Back End Function" 38 | }, 39 | "group": "fn", 40 | "shadow": false 41 | }, 42 | { 43 | "id": "MyTable", 44 | "x": 12.796875, 45 | "y": -126, 46 | "label": "DynamoDB Table\nMyTable", 47 | "title": "My Table", 48 | "model": { 49 | "type": "table", 50 | "description": "My Table" 51 | }, 52 | "group": "table", 53 | "shadow": false 54 | }, 55 | { 56 | "id": "MyBucket", 57 | "x": 40.796875, 58 | "y": 46, 59 | "label": "S3 Bucket\nMyBucket", 60 | "title": "My Bucket", 61 | "model": { 62 | "type": "bucket", 63 | "description": "My Bucket" 64 | }, 65 | "group": "bucket", 66 | "shadow": false 67 | }, 68 | { 69 | "id": "MyStream", 70 | "x": 26.796875, 71 | "y": 199, 72 | "label": "Kinesis Stream\nMyStream", 73 | "title": "My Stream", 74 | "model": { 75 | "type": "stream", 76 | "description": "My Stream" 77 | }, 78 | "group": "stream", 79 | "shadow": false 80 | }, 81 | { 82 | "id": "MyAnalytics", 83 | "x": 142.796875, 84 | "y": 195, 85 | "label": "Kinesis Analytics\nMyAnalytics", 86 | "title": "My Analytics Stream", 87 | "model": { 88 | "type": "analyticsStream", 89 | "description": "My Analytics Stream" 90 | }, 91 | "group": "analyticsStream", 92 | "shadow": false 93 | }, 94 | { 95 | "id": "MyFirehose", 96 | "x": 257.796875, 97 | "y": 209, 98 | "label": "Kinesis Firehose\nMyFirehose", 99 | "title": "My Firehose Stream", 100 | "model": { 101 | "type": "deliveryStream", 102 | "description": "My Firehose Stream" 103 | }, 104 | "group": "deliveryStream", 105 | "shadow": false 106 | }, 107 | { 108 | "id": "MyDBProcessor", 109 | "x": 198.61688250277794, 110 | "y": -199.64783765544206, 111 | "label": "Lambda Function\nMyDBProcessor", 112 | "title": "My Database Processor Function", 113 | "model": { 114 | "type": "fn", 115 | "description": "My Database Processor Function" 116 | }, 117 | "group": "fn", 118 | "shadow": false 119 | }, 120 | { 121 | "id": "MySchedule", 122 | "x": -396.1007836356701, 123 | "y": -203.7255377556265, 124 | "label": "Schedule\nMySchedule", 125 | "title": "My Schedule", 126 | "model": { 127 | "type": "schedule", 128 | "description": "My Schedule" 129 | }, 130 | "group": "schedule", 131 | "shadow": false 132 | }, 133 | { 134 | "id": "MyTopic", 135 | "x": -372.0039122190005, 136 | "y": -65.71618327833689, 137 | "label": "SNS Topic\nMyTopic", 138 | "title": "My Topic", 139 | "model": { 140 | "type": "topic", 141 | "description": "My Topic" 142 | }, 143 | "group": "topic", 144 | "shadow": false 145 | }, 146 | { 147 | "id": "MyScheduledActivities", 148 | "x": -433.3414030977959, 149 | "y": -185.10522802456362, 150 | "label": "Lambda Function\nMyScheduledActivities", 151 | "title": "My Scheduled Activities", 152 | "model": { 153 | "type": "fn", 154 | "description": "My Scheduled Activities" 155 | }, 156 | "group": "fn", 157 | "shadow": false 158 | }, 159 | { 160 | "id": "MyStateMachine", 161 | "x": -820.221039971391, 162 | "y": -176.89564234817388, 163 | "label": "Step Function\nMyStateMachine", 164 | "title": "My State Machine", 165 | "model": { 166 | "type": "stepFn", 167 | "description": "My State Machine" 168 | }, 169 | "group": "stepFn", 170 | "shadow": false 171 | } 172 | ], 173 | "edges": [ 174 | { 175 | "from": "MyIdentity", 176 | "to": "MyApi", 177 | "label": "authorize", 178 | "color": {"color":"blue"}, 179 | "id": "aadc8687-1509-41fa-96b8-1eef0d3f343e" 180 | }, 181 | { 182 | "from": "MyApi", 183 | "to": "MyBackEnd", 184 | "label": "integration", 185 | "color": {"color":"blue"}, 186 | "id": "064f6cb8-0113-4f1d-ad2a-7c46316671cd" 187 | }, 188 | { 189 | "from": "MyBackEnd", 190 | "to": "MyTable", 191 | "label": "read/write", 192 | "color": {"color":"green"}, 193 | "dashes": true, 194 | "id": "cfb93b86-400e-4fce-b3cb-7d5874a24372" 195 | }, 196 | { 197 | "from": "MyBackEnd", 198 | "to": "MyBucket", 199 | "label": "read/write", 200 | "color": {"color":"green"}, 201 | "dashes": true, 202 | "id": "39877ecc-1b3b-4f3a-be7e-27a7eac32753" 203 | }, 204 | { 205 | "from": "MyBackEnd", 206 | "to": "MyStream", 207 | "label": "put", 208 | "color": {"color":"green"}, 209 | "dashes": true, 210 | "id": "971f5ea3-f5b2-4907-8cd3-6a10c799fbdc" 211 | }, 212 | { 213 | "from": "MyStream", 214 | "to": "MyAnalytics", 215 | "label": "input", 216 | "color": {"color":"blue"}, 217 | "id": "505b6654-80fd-4ea4-a0b3-c335efe8321b" 218 | }, 219 | { 220 | "from": "MyAnalytics", 221 | "to": "MyFirehose", 222 | "label": "output", 223 | "color": {"color":"blue"}, 224 | "id": "96cacf24-7571-47cd-9601-8968fdd24216" 225 | }, 226 | { 227 | "from": "MyFirehose", 228 | "to": "MyBucket", 229 | "label": "destination", 230 | "color": {"color":"blue"}, 231 | "id": "1ae0b94c-014b-4f01-9282-5693f9f757b0" 232 | }, 233 | { 234 | "from": "MyTable", 235 | "to": "MyDBProcessor", 236 | "label": "stream", 237 | "color": {"color":"blue"}, 238 | "id": "a78e277f-30bc-4a66-8d8e-dca5e6f51391" 239 | }, 240 | { 241 | "from": "MyDBProcessor", 242 | "to": "MyBucket", 243 | "label": "read/write", 244 | "color": {"color":"green"}, 245 | "dashes": true, 246 | "id": "719bf15e-a4a0-47a5-a4de-448c5d51fc10" 247 | }, 248 | { 249 | "from": "MySchedule", 250 | "to": "MyScheduledActivities", 251 | "label": "target", 252 | "color": {"color":"blue"}, 253 | "id": "3bee92fd-f2b7-4cc0-a6da-b87f5e73eca4" 254 | }, 255 | { 256 | "from": "MyScheduledActivities", 257 | "to": "MyTable", 258 | "label": "read/write", 259 | "color": {"color":"green"}, 260 | "dashes": true, 261 | "id": "6979d2a2-54c9-4dd8-b69d-04e07a8c4274" 262 | }, 263 | { 264 | "from": "MyTopic", 265 | "to": "MyScheduledActivities", 266 | "label": "trigger", 267 | "color": {"color":"blue"}, 268 | "id": "fc9492bd-6de5-4888-898a-2ff04c1a0c6c" 269 | }, 270 | { 271 | "from": "MyScheduledActivities", 272 | "to": "MyStateMachine", 273 | "label": "activity", 274 | "color": {"color":"green"}, 275 | "dashes": true, 276 | "id": "ab9c21ef-5dc7-49ed-84ba-e2b67f3ce962" 277 | }, 278 | { 279 | "from": "MyStateMachine", 280 | "to": "MyBackEnd", 281 | "label": "invoke", 282 | "color": {"color":"blue"}, 283 | "id": "698e9074-e122-4d78-a91d-4b2eb593dfbc" 284 | } 285 | ] 286 | } -------------------------------------------------------------------------------- /dist/examples/iot.json: -------------------------------------------------------------------------------- 1 | {"nodes":[{"id":"i1","x":-314,"y":-184.5,"label":"IoT Topic Rule\ni1","model":{"type":"iotRule","description":""},"group":"iotRule","shadow":false},{"id":"f1","x":-67,"y":-156.5,"label":"Lambda Function\nf1","model":{"type":"fn","description":""},"group":"fn","shadow":false},{"id":"b1","x":120,"y":-127.5,"label":"S3 Bucket\nb1","model":{"type":"bucket","description":""},"group":"bucket","shadow":false}],"edges":[{"from":"i1","to":"f1","label":"invoke","color":{"color":"blue"},"id":"68ce9cac-8a9a-4553-94dd-af56457b5cfc"},{"from":"f1","to":"b1","label":"read/write","color":{"color":"green"},"dashes":true,"id":"67986134-9fd3-40bd-9cb9-a46b30ef121e"},{"from":"i1","to":"i1","label":"republish","color":{"color":"blue"},"id":"d6753dda-23e4-400c-be53-15df9c8269cd"}]} -------------------------------------------------------------------------------- /dist/examples/s3-processing.json: -------------------------------------------------------------------------------- 1 | {"nodes":[{"id":"SourceBucket","x":-457,"y":-64.5,"label":"S3 Bucket\nSourceBucket","title":"The Source Bucket","model":{"type":"bucket","description":"The Source Bucket"},"group":"bucket","shadow":false},{"id":"DestBucket","x":-68,"y":-62.5,"label":"S3 Bucket\nDestBucket","title":"The Destination Bucket","model":{"type":"bucket","description":"The Destination Bucket"},"group":"bucket","shadow":false},{"id":"DataProc","x":-284,"y":-84.5,"label":"Lambda Function\nDataProc","title":"The Data Processing function","model":{"type":"fn","description":"The Data Processing function"},"group":"fn","shadow":false}],"edges":[{"from":"SourceBucket","to":"DataProc","label":"trigger","color":{"color":"blue"},"id":"18a2c25d-446c-4c38-9a6a-65d2fe135d36"},{"from":"DataProc","to":"DestBucket","label":"read/write","color":{"color":"green"},"dashes":true,"id":"aa602462-a8d9-4b92-a307-71a665cc924a"}]} -------------------------------------------------------------------------------- /dist/examples/stream-test.json: -------------------------------------------------------------------------------- 1 | {"nodes":[{"id":"api","x":-228.203125,"y":-154.5,"label":"API Gateway\napi","model":{"type":"api","description":""},"group":"api","shadow":false},{"id":"f1","x":-41.203125,"y":-40.5,"label":"Lambda Function\nf1","model":{"type":"fn","description":""},"group":"fn","shadow":false},{"id":"ks","x":128.796875,"y":12.5,"label":"Kinesis Stream\nks","model":{"type":"stream","description":""},"group":"stream","shadow":false},{"id":"ka","x":254.796875,"y":14.5,"label":"Kinesis Analytics\nka","model":{"type":"analyticsStream","description":""},"group":"analyticsStream","shadow":false},{"id":"kf","x":372.796875,"y":26.5,"label":"Kinesis Firehose\nkf","model":{"type":"deliveryStream","description":""},"group":"deliveryStream","shadow":false},{"id":"b1","x":-220.203125,"y":131.5,"label":"S3 Bucket\nb1","model":{"type":"bucket","description":""},"group":"bucket","shadow":false}],"edges":[{"from":"api","to":"f1","label":"integration","color":{"color":"blue"},"id":"efc8a873-8adc-474c-b94f-edd668cbe6ed"},{"from":"f1","to":"ks","label":"put","color":{"color":"green"},"dashes":true,"id":"1facaaa6-5777-4047-9501-02d319b9748a"},{"from":"ks","to":"ka","label":"input","color":{"color":"blue"},"id":"4593fba5-0f35-4d41-9f31-8be61690e4ef"},{"from":"ka","to":"kf","label":"output","color":{"color":"blue"},"id":"78bb14a7-2b39-4ebd-a248-01f4b3039e50"},{"from":"kf","to":"b1","label":"destination","color":{"color":"blue"},"id":"bac5a755-72e3-4f45-8c4e-f120d435fdaf"}]} -------------------------------------------------------------------------------- /dist/img/aws/AWS-Lambda_Function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/AWS-Lambda_Function.png -------------------------------------------------------------------------------- /dist/img/aws/AWS-Step-Functions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/AWS-Step-Functions.png -------------------------------------------------------------------------------- /dist/img/aws/Amazon-API-Gateway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-API-Gateway.png -------------------------------------------------------------------------------- /dist/img/aws/Amazon-CloudWatch_Event-Time-Based.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-CloudWatch_Event-Time-Based.png -------------------------------------------------------------------------------- /dist/img/aws/Amazon-Cognito.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-Cognito.png -------------------------------------------------------------------------------- /dist/img/aws/Amazon-DynamoDB_Table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-DynamoDB_Table.png -------------------------------------------------------------------------------- /dist/img/aws/Amazon-Kinesis-Data-Analytics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-Kinesis-Data-Analytics.png -------------------------------------------------------------------------------- /dist/img/aws/Amazon-Kinesis-Data-Firehose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-Kinesis-Data-Firehose.png -------------------------------------------------------------------------------- /dist/img/aws/Amazon-Kinesis-Data-Streams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-Kinesis-Data-Streams.png -------------------------------------------------------------------------------- /dist/img/aws/Amazon-S3_Bucket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-S3_Bucket.png -------------------------------------------------------------------------------- /dist/img/aws/Amazon-SNS_Topic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-SNS_Topic.png -------------------------------------------------------------------------------- /dist/img/aws/IoT_Rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/IoT_Rule.png -------------------------------------------------------------------------------- /dist/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/logo.png -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Serverless by Design 6 | 7 | 8 | 9 | 10 | 11 | 12 | 47 | 48 | 49 | 50 | 51 |
52 | 106 |
107 |
108 | 109 | 148 | 149 | 173 | 174 | 231 | 232 |
233 | 234 | 235 | 236 | 244 | 245 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new-repo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "webpack --devtool source-map", 9 | "build": "webpack -p" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "css-loader": "^2.1.1", 16 | "file-loader": "^3.0.1", 17 | "image-webpack-loader": "^4.6.0", 18 | "style-loader": "^0.23.1", 19 | "webpack": "^4.29.6", 20 | "webpack-cli": "^3.3.2" 21 | }, 22 | "dependencies": { 23 | "bootstrap": "^4.3.1", 24 | "file-saver": "^2.0.1", 25 | "jquery": ">=3.4.0", 26 | "js-yaml": "^3.13.0", 27 | "jszip": "^3.2.1", 28 | "popper.js": "^1.15.0", 29 | "vis": "^4.21.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/engines/sam.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const jsyaml = require('js-yaml'); 4 | 5 | const runtimes = { 6 | "nodejs8.10": { 7 | fileExtension: "js", 8 | handler: "handler", 9 | startingCode: 10 | `'use strict'; 11 | 12 | console.log("Loading function"); 13 | 14 | exports.handler = async (event, context) => { 15 | console.log('Received event:', JSON.stringify(event, null, 2)); 16 | return "Hello World"; 17 | // or 18 | // throw new Error(“some error type”); 19 | }; 20 | 21 | ` 22 | }, 23 | "python3.7": { 24 | fileExtension: "py", 25 | handler: "lambda_handler", 26 | startingCode: 27 | `import json 28 | 29 | print("Loading function") 30 | 31 | def lambda_handler(event, context): 32 | print("Received event: " + json.dumps(event, indent=2)) 33 | return "Hello World" 34 | or 35 | #raise Exception("Something went wrong") 36 | ` 37 | } 38 | } 39 | 40 | var renderingRules = { 41 | bucket: { 42 | resource: function (status, node) { 43 | status.template.Resources[node.id] = { 44 | Type: "AWS::S3::Bucket" 45 | } 46 | }, 47 | event: function (status, id, idFrom) { 48 | status.template.Resources[id].Properties.Events['Bucket' + idFrom] = { 49 | Type: "S3", 50 | Properties: { 51 | Bucket: "!Ref " + idFrom, 52 | Events: "s3:ObjectCreated:*" 53 | } 54 | }; 55 | // To avoid circular dependencies with a more specific policy 56 | status.template.Resources[id].Properties.Policies.push('AmazonS3ReadOnlyAccess'); 57 | }, 58 | policy: function (status, id, idTo) { 59 | return { 60 | Effect: "Allow", 61 | Action: ["s3:GetObject", "s3:PutObject"], 62 | Resource: "!Sub ${" + idTo + ".Arn}/*" 63 | }; 64 | }, 65 | }, 66 | table: { 67 | resource: function (status, node) { 68 | status.template.Resources[node.id] = { 69 | Type: "AWS::DynamoDB::Table", 70 | Properties: { 71 | AttributeDefinitions: [ 72 | { 73 | AttributeName: "id", 74 | AttributeType: "S" 75 | }, 76 | { 77 | AttributeName: "version", 78 | AttributeType: "N" 79 | } 80 | ], 81 | KeySchema: [ 82 | { 83 | AttributeName: "id", 84 | KeyType: "HASH" 85 | }, 86 | { 87 | AttributeName: "version", 88 | KeyType: "RANGE" 89 | } 90 | ], 91 | BillingMode: 'PAY_PER_REQUEST', 92 | StreamSpecification: { 93 | StreamViewType: "NEW_AND_OLD_IMAGES" 94 | } 95 | } 96 | }; 97 | }, 98 | event: function (status, id, idFrom) { 99 | status.template.Resources[id].Properties.Events['Table' + idFrom] = { 100 | Type: "DynamoDB", 101 | Properties: { 102 | Stream: "!GetAtt " + idFrom + ".StreamArn", 103 | StartingPosition: "TRIM_HORIZON", 104 | BatchSize: 10 105 | } 106 | }; 107 | }, 108 | policy: function (status, id, idTo) { 109 | return { 110 | Effect: "Allow", 111 | Action: ["dynamodb:GetItem", "dynamodb:PutItem"], 112 | Resource: "!GetAtt " + idTo + ".Arn" 113 | }; 114 | }, 115 | }, 116 | api: { 117 | resource: function (status, node) { 118 | // Nothing to do, created by the API event 119 | }, 120 | event: function (status, id, idFrom) { 121 | status.template.Resources[id].Properties.Events['Api' + idFrom] = { 122 | Type: "Api", 123 | Properties: { 124 | Path: "/{proxy+}", 125 | Method: "ANY" 126 | } 127 | }; 128 | }, 129 | policy: function (status, id, idTo) { 130 | return { 131 | Effect: "Allow", 132 | Action: "execute-api:Invoke", 133 | Resource: "!Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:*/*/*/*" 134 | }; 135 | }, 136 | }, 137 | stream: { 138 | resource: function (status, node) { 139 | status.template.Resources[node.id] = { 140 | Type: "AWS::Kinesis::Stream", 141 | Properties: { 142 | ShardCount: 1 143 | } 144 | }; 145 | }, 146 | event: function (status, id, idFrom) { 147 | status.template.Resources[id].Properties.Events['Stream' + idFrom] = { 148 | Type: "Kinesis", 149 | Properties: { 150 | Stream: "!GetAtt " + idFrom + ".Arn", 151 | StartingPosition: "TRIM_HORIZON", 152 | BatchSize: 10 153 | } 154 | }; 155 | }, 156 | policy: function (status, id, idTo) { 157 | return { 158 | Effect: "Allow", 159 | Action: ["kinesis:PutRecord", "kinesis:PutRecords"], 160 | Resource: "!GetAtt " + idTo + ".Arn" 161 | }; 162 | }, 163 | }, 164 | deliveryStream: { 165 | resource: function (status, node) { 166 | var targetBucketId = null; 167 | var targetFnId = null; 168 | node.to.forEach(function (idTo) { // Target resources 169 | var node_to = status.model.nodes[idTo]; 170 | if (node_to.type === 'bucket') { 171 | targetBucketId = idTo; 172 | } else if (node_to.type === 'fn') { 173 | targetFnId = idTo; 174 | } 175 | }); 176 | if (targetBucketId == null) { 177 | console.error("Delivery Stream without a destination"); 178 | return; 179 | } 180 | var deliveryPolicyId = node.id + "DeliveryPolicy"; 181 | var deliveryRoleId = node.id + "DeliveryRole"; 182 | // Create the Delivery Strem 183 | status.template.Resources[node.id] = { 184 | DependsOn: [deliveryPolicyId], 185 | Type: 'AWS::KinesisFirehose::DeliveryStream', 186 | Properties: { 187 | ExtendedS3DestinationConfiguration: { 188 | BucketARN: "!GetAtt " + targetBucketId + ".Arn", 189 | BufferingHints: { 190 | IntervalInSeconds: 60, 191 | SizeInMBs: 50 192 | }, 193 | CompressionFormat: "UNCOMPRESSED", 194 | Prefix: "firehose/", 195 | RoleARN: "!GetAtt " + deliveryRoleId + ".Arn" 196 | } 197 | } 198 | }; 199 | if (targetFnId !== null) { 200 | status.template.Resources[node.id].Properties 201 | .ExtendedS3DestinationConfiguration.ProcessingConfiguration = { 202 | Enabled: true, 203 | Processors: [{ 204 | Parameters: [{ 205 | ParameterName: "LambdaArn", 206 | ParameterValue: "!GetAtt " + targetFnId + ".Arn" 207 | }], 208 | Type: "Lambda" 209 | }] 210 | } 211 | } 212 | // Create a delivery role 213 | status.template.Resources[deliveryRoleId] = { 214 | Type: 'AWS::IAM::Role', 215 | Properties: { 216 | AssumeRolePolicyDocument: { 217 | Version: '2012-10-17', 218 | Statement: [{ 219 | Effect: "Allow", 220 | Principal: { Service: "firehose.amazonaws.com" }, 221 | Action: 'sts:AssumeRole', 222 | Condition: { 223 | StringEquals: { 224 | 'sts:ExternalId': "!Ref AWS::AccountId" 225 | } 226 | } 227 | }] 228 | } 229 | } 230 | }; 231 | // Create a delivery policy for the role 232 | status.template.Resources[deliveryPolicyId] = { 233 | Type: 'AWS::IAM::Policy', 234 | Properties: { 235 | PolicyName: "firehose_delivery_policy", 236 | PolicyDocument: { 237 | Version: '2012-10-17', 238 | Statement: [{ 239 | Effect: 'Allow', 240 | Action: [ 241 | 's3:AbortMultipartUpload', 242 | 's3:GetBucketLocation', 243 | 's3:GetObject', 244 | 's3:ListBucket', 245 | 's3:ListBucketMultipartUploads', 246 | 's3:PutObject' 247 | ], 248 | Resource: [ 249 | "!GetAtt " + targetBucketId + ".Arn" 250 | ] 251 | }] 252 | }, 253 | Roles: ["!Ref " + deliveryRoleId] 254 | } 255 | }; 256 | }, 257 | event: function () { }, // TODO 258 | policy: function (status, id, idTo) { 259 | return { 260 | Effect: "Allow", 261 | Action: [ 262 | "firehose:PutRecord", 263 | "firehose:PutRecordBatch" 264 | ], 265 | // Kinesis Firehose ARN syntax (can't use GetAtt) 266 | // arn:aws:firehose:region:account-id:deliverystream/delivery-stream-name 267 | Resource: "!Sub arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/${" + idTo + "}", 268 | }; 269 | }, 270 | }, 271 | analyticsStream: { 272 | resource: function (status, node) { 273 | 274 | // Input resources 275 | var inputStreamId = null; 276 | var inputDeliveryStreamId = null; 277 | node.from.forEach(function (idFrom) { 278 | var node_from = status.model.nodes[idFrom]; 279 | if (node_from.type === 'stream') { 280 | inputStreamId = idFrom; 281 | } else if (node_from.type === 'deliveryStream') { 282 | inputDeliveryStreamId = idFrom; 283 | } 284 | }); 285 | 286 | // Output resource 287 | var outputStreamId = null; 288 | var outputDeliveryStreamId = null; 289 | node.to.forEach(function (idTo) { 290 | var node_to = status.model.nodes[idTo]; 291 | if (node_to.type === 'stream') { 292 | outputStreamId = idTo; 293 | } else if (node_to.type === 'deliveryStream') { 294 | outputDeliveryStreamId = idTo; 295 | } 296 | }); 297 | 298 | var analyticsStreamRoleId = node.id + "Role"; 299 | var analyticsStreamOutputId = node.id + "Outputs"; 300 | 301 | status.template.Resources[node.id] = { 302 | Type: "AWS::KinesisAnalytics::Application", 303 | Properties: { 304 | ApplicationName: node.id, 305 | Inputs: [{ 306 | NamePrefix: "exampleNamePrefix", 307 | InputSchema: { 308 | RecordColumns: [{ 309 | Name: "example", 310 | SqlType: "VARCHAR(16)", 311 | Mapping: "$.example" 312 | }], 313 | RecordFormat: { 314 | RecordFormatType: "JSON", 315 | MappingParameters: { 316 | JSONMappingParameters: { 317 | RecordRowPath: "$" 318 | } 319 | } 320 | } 321 | } 322 | }] 323 | } 324 | }; 325 | 326 | if (node.description !== '') { 327 | status.template.Resources[node.id] 328 | .Properties.ApplicationDescription = node.description; 329 | } 330 | 331 | if (inputStreamId !== null) { 332 | status.template.Resources[node.id] 333 | .Properties.Inputs[0].KinesisStreamsInput = { 334 | ResourceARN: "!GetAtt " + inputStreamId + ".Arn", 335 | RoleARN: "!GetAtt " + analyticsStreamRoleId + ".Arn" 336 | }; 337 | } 338 | 339 | if (inputDeliveryStreamId !== null) { 340 | status.template.Resources[node.id] 341 | .Properties.Inputs[0].KinesisFirehoseInput = { 342 | // Kinesis Firehose ARN syntax (can't use GetAtt) 343 | // arn:aws:firehose:region:account-id:deliverystream/delivery-stream-name 344 | ResourceARN: "!Sub arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/${" + inputDeliveryStreamId + "}", 345 | RoleARN: "!GetAtt " + analyticsStreamRoleId + ".Arn" 346 | }; 347 | } 348 | 349 | status.template.Resources[analyticsStreamRoleId] = { 350 | Type: "AWS::IAM::Role", 351 | Properties: { 352 | AssumeRolePolicyDocument: { 353 | Version: "2012-10-17", 354 | Statement: [{ 355 | Effect: "Allow", 356 | Principal: { 357 | Service: "kinesisanalytics.amazonaws.com" 358 | }, 359 | Action: "sts:AssumeRole" 360 | }] 361 | }, 362 | Path: "/", 363 | Policies: [{ 364 | PolicyName: "Open", 365 | PolicyDocument: { 366 | Version: "2012-10-17", 367 | Statement: [{ 368 | Effect: "Allow", 369 | Action: "*", 370 | Resource: "*" 371 | }] 372 | } 373 | }] 374 | } 375 | }; 376 | 377 | status.template.Resources[analyticsStreamOutputId] = { 378 | Type: "AWS::KinesisAnalytics::ApplicationOutput", 379 | DependsOn: node.id, 380 | Properties: { 381 | ApplicationName: "!Ref " + node.id, 382 | Output: { 383 | Name: "exampleOutput", 384 | DestinationSchema: { 385 | RecordFormatType: "CSV" 386 | } 387 | } 388 | } 389 | }; 390 | 391 | if (outputStreamId !== null) { 392 | status.template.Resources[analyticsStreamOutputId] 393 | .Properties.Output.KinesisStreamsOutput = { 394 | ResourceARN: "!GetAtt " + outputStreamId + ".Arn", 395 | RoleARN: "!GetAtt " + analyticsStreamRoleId + ".Arn" 396 | }; 397 | } 398 | 399 | if (outputDeliveryStreamId !== null) { 400 | status.template.Resources[analyticsStreamOutputId] 401 | .Properties.Output.KinesisFirehoseOutput = { 402 | // Kinesis Firehose ARN syntax (can't use GetAtt) 403 | // arn:aws:firehose:region:account-id:deliverystream/delivery-stream-name 404 | ResourceARN: "!Sub arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/${" + outputDeliveryStreamId + "}", 405 | RoleARN: "!GetAtt " + analyticsStreamRoleId + ".Arn" 406 | }; 407 | } 408 | 409 | }, 410 | event: function () { }, // TODO 411 | policy: function () { } // TODO 412 | }, 413 | schedule: { 414 | resource: function (status, node) { 415 | // Nothing to do 416 | }, 417 | event: function (status, id, idFrom) { 418 | status.template.Resources[id].Properties.Events['Schedule' + idFrom] = { 419 | Type: "Schedule", 420 | Properties: { 421 | Schedule: "rate(5 minutes)" 422 | } 423 | }; 424 | }, 425 | policy: function () { } // This has no sense 426 | }, 427 | topic: { 428 | resource: function (status, node) { 429 | status.template.Resources[node.id] = { 430 | Type: "AWS::SNS::Topic" 431 | }; 432 | }, 433 | event: function (status, id, idFrom) { 434 | status.template.Resources[id].Properties.Events['Topic' + idFrom] = { 435 | Type: "SNS", 436 | Properties: { 437 | Topic: "!Ref " + idFrom 438 | } 439 | }; 440 | }, 441 | policy: function (status, id, idTo) { 442 | return { 443 | Effect: "Allow", 444 | Action: "sns:Publish", 445 | Resource: "!Ref " + idTo // For an SNS topic, it returns the ARN 446 | }; 447 | }, 448 | }, 449 | fn: { 450 | resource: function (status, node) { 451 | status.template.Resources[node.id] = { 452 | Type: "AWS::Serverless::Function", 453 | Properties: { 454 | // FunctionName: node.id, 455 | Handler: node.id + "." + runtimes[status.runtime].handler, 456 | Runtime: status.runtime, 457 | CodeUri: ".", 458 | Policies: [] 459 | } 460 | }; 461 | if (node.description !== '') { 462 | status.template.Resources[node.id].Properties.Description = node.description; 463 | } 464 | status.files[node.id + '.' + runtimes[status.runtime].fileExtension] = 465 | runtimes[status.runtime].startingCode; 466 | if (node.from.length > 0) { // There are triggers for this function 467 | status.template.Resources[node.id].Properties.Events = {}; 468 | node.from.forEach(function (idFrom) { 469 | console.log("Trigger " + idFrom + " -> " + node.id); 470 | renderingRules[status.model.nodes[idFrom].type].event(status, node.id, idFrom); 471 | }); 472 | } 473 | if (node.to.length > 0) { // There are resources target of this function 474 | var policy = { 475 | Version: "2012-10-17", 476 | Statement: [] 477 | }; 478 | node.to.forEach(function (idTo) { 479 | console.log("Policy " + node.id + " -> " + idTo); 480 | policy.Statement.push( 481 | renderingRules[status.model.nodes[idTo].type] 482 | .policy(status, node.id, idTo) 483 | ); 484 | }); 485 | status.template.Resources[node.id].Properties.Policies.push(policy); 486 | } 487 | }, 488 | event: function () { }, // Nothing to do, this is not a trigger, but a fn to fn invocation 489 | policy: function (status, id, idTo) { 490 | return { 491 | Effect: "Allow", 492 | Action: ["lambda:Invoke", "lambda:InvokeAsync"], 493 | Resource: "!GetAtt " + idTo + ".Arn" 494 | }; 495 | } 496 | }, 497 | stepFn: { 498 | resource: function (status, node) { 499 | status.template.Resources[node.id] = { 500 | Type: "AWS::StepFunctions::StateMachine", 501 | Properties: { 502 | // The DefinitionString is added later 503 | // This role is automatically created by the AWS console 504 | // the first time you create a state machine in a region 505 | RoleArn: "!Sub arn:aws:iam::${AWS::AccountId}:role/service-role/StatesExecutionRole-${AWS::Region}" 506 | }, 507 | }; 508 | var definitionString = { 509 | Comment: "A Hello World example", 510 | StartAt: "HelloWorld", 511 | States: { 512 | HelloWorld: { 513 | Type: "Pass", 514 | Result: "Hello World!", 515 | End: true 516 | } 517 | } 518 | }; 519 | // The DefinitionString must be a string with JSON syntax within the template 520 | status.template.Resources[node.id].Properties.DefinitionString = 521 | JSON.stringify(definitionString, null, 2); 522 | }, 523 | event: function () { }, // Nothing to do 524 | policy: function (status, id, idTo) { 525 | return { 526 | "Effect": "Allow", 527 | "Action": [ 528 | "states:DescribeExecution", 529 | "states:GetExecutionHistory", 530 | "states:ListExecutions", 531 | "states:StartExecution", 532 | "states:StopExecution" 533 | ], 534 | "Resource": [ 535 | "!Ref " + idTo 536 | ] 537 | } 538 | } 539 | }, 540 | cognitoIdentity: { 541 | resource: function (status, node) { 542 | var cognitoUnauthRoleId = node.id + "CognitoUnauthRole"; 543 | var cognitoUnauthPolicyId = node.id + "CognitoUnauthPolicy"; 544 | status.template.Resources[node.id] = { 545 | Type: "AWS::Cognito::IdentityPool", 546 | Properties: { 547 | AllowUnauthenticatedIdentities: true // TODO Maybe this is not a secure default ??? 548 | } 549 | } 550 | status.template.Resources[cognitoUnauthRoleId] = { 551 | Type: 'AWS::IAM::Role', 552 | Properties: { 553 | AssumeRolePolicyDocument: { 554 | Version: '2012-10-17', 555 | Statement: [{ 556 | Effect: "Allow", 557 | Principal: { Federated: "cognito-identity.amazonaws.com" }, 558 | Action: 'sts:AssumeRoleWithWebIdentity', 559 | Condition: { 560 | StringEquals: { 561 | "cognito-identity.amazonaws.com:aud": "!Ref " + node.id 562 | }, 563 | "ForAnyValue:StringLike": { 564 | "cognito-identity.amazonaws.com:amr": "unauthenticated" 565 | } 566 | } 567 | }] 568 | } 569 | } 570 | } 571 | // Create a delivery policy for the role 572 | status.template.Resources[cognitoUnauthPolicyId] = { 573 | Type: 'AWS::IAM::Policy', 574 | Properties: { 575 | PolicyName: "cognito_unauth_policy", 576 | PolicyDocument: { 577 | Version: '2012-10-17', 578 | Statement: [] 579 | }, 580 | Roles: ["!Ref " + cognitoUnauthRoleId] 581 | } 582 | }; 583 | // Output resources 584 | node.to.forEach(function (idTo) { 585 | var node_to = status.model.nodes[idTo]; 586 | status.template.Resources[cognitoUnauthPolicyId] 587 | .Properties.PolicyDocument.Statement.push( 588 | renderingRules[status.model.nodes[idTo].type] 589 | .policy(status, node.id, idTo) 590 | ); 591 | }); 592 | }, 593 | event: function () { }, // TODO ??? 594 | policy: function () { } // TODO ??? 595 | }, 596 | iotRule: { 597 | resource: function (status, node) { 598 | status.template.Resources[node.id] = { 599 | Type: "AWS::IoT::TopicRule", 600 | Properties: { 601 | TopicRulePayload: { 602 | RuleDisabled: "true", // safe choice 603 | Sql: "Select temp FROM 'Some/Topic' WHERE temp > 60", 604 | Actions: [] 605 | } 606 | } 607 | } 608 | if (node.description !== '') { 609 | status.template.Resources[node.id].Properties.TopicRulePayload.Description = node.description; 610 | } 611 | // Output resources 612 | node.to.forEach(function (idTo) { 613 | var node_to = status.model.nodes[idTo]; 614 | switch (node_to.type) { 615 | case 'fn': 616 | status.template.Resources[node.id].Properties.TopicRulePayload.Actions.push({ 617 | Lambda: { 618 | FunctionArn: "!GetAtt " + idTo + ".Arn" 619 | } 620 | }); 621 | break; 622 | case 'iotRule': // republish 623 | var republishRoleId = idTo + "PublishRole"; 624 | status.template.Resources[node.id].Properties.TopicRulePayload.Actions.push({ 625 | Republish: { 626 | Topic: "Output/Topic", 627 | RoleArn: "!GetAtt " + republishRoleId + ".Arn" 628 | } 629 | }); 630 | status.template.Resources[republishRoleId] = { 631 | Type: "AWS::IAM::Role", 632 | Properties: { 633 | AssumeRolePolicyDocument: { 634 | Version: "2012-10-17", 635 | Statement: [{ 636 | Effect: "Allow", 637 | Action: [ "sts:AssumeRole" ], 638 | Principal: { 639 | Service: [ "iot.amazonaws.com" ] 640 | } 641 | }] 642 | }, 643 | Policies: [{ 644 | PolicyName: "publish", 645 | PolicyDocument: { 646 | Version: "2012-10-17", 647 | Statement: [{ 648 | Effect: "Allow", 649 | Action: "iot:Publish", 650 | Resource: "!Sub arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/Output/*" 651 | }] 652 | } 653 | }] 654 | } 655 | }; 656 | break; 657 | default: 658 | throw "Error: connection type not supported (" + node_to.type + ")"; 659 | } 660 | }); 661 | }, 662 | event: function () { }, 663 | policy: function () { } 664 | } 665 | }; 666 | 667 | function render(model, runtime, deployment) { 668 | console.log('Using SAM...'); 669 | var files = {}; 670 | var template = { 671 | AWSTemplateFormatVersion: "2010-09-09", 672 | Transform: "AWS::Serverless-2016-10-31" 673 | }; 674 | 675 | if (deployment) { 676 | template.Globals = { 677 | Function: { 678 | AutoPublishAlias: "live", 679 | DeploymentPreference: { 680 | Type: deployment 681 | } 682 | } 683 | } 684 | } 685 | 686 | var status = { 687 | model: model, 688 | runtime: runtime, 689 | files: files, 690 | template: template 691 | } 692 | 693 | template.Resources = {}; 694 | for (var id in model.nodes) { 695 | var node = model.nodes[id]; 696 | renderingRules[node.type].resource(status, node); 697 | } 698 | 699 | console.log(template); // Still in JSON 700 | console.log(JSON.stringify(template, null, 4)); // JSON -> text 701 | 702 | for (var r in template.Resources) { 703 | console.log(r + " -> YAML"); 704 | console.log(jsyaml.safeDump(template.Resources[r], { lineWidth: 1024 })); 705 | } 706 | 707 | // Line breaks can introduce YAML syntax (e.g. >-) that will put some variables 708 | // (e.g. AWS::Region) between quotes. 709 | // Single quotes must be removed for functions (e.g. !Ref) to work. 710 | files['template.yaml'] = jsyaml.safeDump(template, { lineWidth: 1024 }).replace(/'(!.+)'/g, "$1"); 711 | 712 | return files; 713 | } 714 | 715 | module.exports = render; -------------------------------------------------------------------------------- /src/engines/servfrmwk.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const jsyaml = require('js-yaml'); 4 | 5 | const runtimes = { 6 | "nodejs8.10": { 7 | fileExtension: "js", 8 | gitignore: `# package directories 9 | node_modules 10 | jspm_packages 11 | 12 | # Serverless directories 13 | .serverless 14 | `, 15 | handler: "handler", 16 | startingCode: 17 | `'use strict'; 18 | 19 | module.exports.handler = (event, context, callback) => { 20 | const response = { 21 | statusCode: 200, 22 | body: JSON.stringify({ 23 | message: 'Go Serverless v1.0! Your function executed successfully!', 24 | input: event, 25 | }), 26 | }; 27 | 28 | callback(null, response); 29 | 30 | // Use this code if you don't use the http event with the LAMBDA-PROXY integration 31 | // callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event }); 32 | }; 33 | ` 34 | }, 35 | "python3.7": { 36 | fileExtension: "py", 37 | gitignore: `# Distribution / packaging 38 | .Python 39 | env/ 40 | build/ 41 | develop-eggs/ 42 | dist/ 43 | downloads/ 44 | eggs/ 45 | .eggs/ 46 | lib/ 47 | lib64/ 48 | parts/ 49 | sdist/ 50 | var/ 51 | *.egg-info/ 52 | .installed.cfg 53 | *.egg 54 | 55 | # Serverless directories 56 | .serverless 57 | `, 58 | handler: "handler", 59 | startingCode: 60 | `import json 61 | 62 | def handler(event, context): 63 | body = { 64 | "message": "Go Serverless v1.0! Your function executed successfully!", 65 | "input": event 66 | } 67 | 68 | response = { 69 | "statusCode": 200, 70 | "body": json.dumps(body) 71 | } 72 | 73 | return response 74 | 75 | # Use this code if you don't use the http event with the LAMBDA-PROXY integration 76 | """ 77 | return { 78 | "message": "Go Serverless v1.0! Your function executed successfully!", 79 | "event": event 80 | } 81 | """ 82 | ` 83 | } 84 | }; 85 | 86 | var renderingRules = { 87 | bucket: { 88 | resource: function (status, node) { 89 | status.template.resources.Resources[node.id] = { 90 | Type: "AWS::S3::Bucket" 91 | }; 92 | }, 93 | event: function (status, id, idFrom) { 94 | if (status.model.nodes[id].type === 'fn') { 95 | status.template.functions[id].events.push({ 96 | s3: { 97 | bucket: idFrom, 98 | event: "s3:ObjectCreated:*" 99 | } 100 | }); 101 | } else { 102 | status.template.resources.Resources[id].Properties.Events['Bucket' + idFrom] = { 103 | Type: "S3", 104 | Properties: { 105 | Bucket: { Ref: idFrom }, 106 | Events: "s3:ObjectCreated:*" 107 | } 108 | }; 109 | 110 | // To avoid circular dependencies with a more specific policy 111 | status.template.resources.Resources[id].Properties.Policies.push('AmazonS3ReadOnlyAccess'); 112 | } 113 | }, 114 | policy: function (status, id, idTo) { 115 | return { 116 | Effect: "Allow", 117 | Action: ["s3:GetObject", "s3:PutObject"], 118 | Resource: { 119 | "Fn::Join": [ 120 | "", 121 | [ 122 | { "Fn::GetAtt": [idTo, "Arn"] }, 123 | "/*" 124 | ] 125 | ] 126 | } 127 | }; 128 | }, 129 | }, 130 | table: { 131 | resource: function (status, node) { 132 | status.template.resources.Resources[node.id] = { 133 | Type: "AWS::DynamoDB::Table", 134 | Properties: { 135 | AttributeDefinitions: [ 136 | { 137 | AttributeName: "id", 138 | AttributeType: "S" 139 | }, 140 | { 141 | AttributeName: "version", 142 | AttributeType: "N" 143 | } 144 | ], 145 | KeySchema: [ 146 | { 147 | AttributeName: "id", 148 | KeyType: "HASH" 149 | }, 150 | { 151 | AttributeName: "version", 152 | KeyType: "RANGE" 153 | } 154 | ], 155 | BillingMode: 'PAY_PER_REQUEST', 156 | StreamSpecification: { 157 | StreamViewType: "NEW_AND_OLD_IMAGES" 158 | } 159 | } 160 | }; 161 | }, 162 | event: function (status, id, idFrom) { 163 | if (status.model.nodes[id].type === 'fn') { 164 | status.template.functions[id].events.push({ 165 | stream: { 166 | type: "dynamodb", 167 | arn: { "Fn::GetAtt": [idFrom, "StreamArn"] } 168 | } 169 | }); 170 | } else { 171 | status.template.resources.Resources[id].Properties.Events['Table' + idFrom] = { 172 | Type: "DynamoDB", 173 | Properties: { 174 | Stream: { "Fn::GetAtt": [idFrom, "StreamArn"] }, 175 | StartingPosition: "TRIM_HORIZON", 176 | BatchSize: 10 177 | } 178 | }; 179 | } 180 | }, 181 | policy: function (status, id, idTo) { 182 | return { 183 | Effect: "Allow", 184 | Action: ["dynamodb:GetItem", "dynamodb:PutItem"], 185 | Resource: { "Fn::GetAtt": [idTo, "Arn"] } 186 | }; 187 | }, 188 | }, 189 | api: { 190 | resource: function (status, node) { 191 | // Nothing to do, created by the API event 192 | }, 193 | event: function (status, id, idFrom) { 194 | if (status.model.nodes[id].type === 'fn') { 195 | status.template.functions[id].events.push({ 196 | http: { 197 | path: "/{proxy+}", 198 | method: "get" 199 | } 200 | }); 201 | } else { // Currently this doesn't execute but could if Step Functions handled events 202 | status.template.resources.Resources[id].Properties.Events['Api' + idFrom] = { 203 | Type: "Api", 204 | Properties: { 205 | Path: "/{proxy+}", 206 | Method: "ANY" 207 | } 208 | }; 209 | } 210 | }, 211 | policy: function (status, id, idTo) { 212 | return { 213 | Effect: "Allow", 214 | Action: "execute-api:Invoke", 215 | Resource: { 216 | "Fn::Join": [ 217 | "", 218 | [ 219 | "arn:aws:execute-api:", 220 | { Ref: "AWS::Region" }, 221 | ":", 222 | { Ref: "AWS::AccountId" }, 223 | ":*/*/*/*" 224 | ] 225 | ] 226 | } 227 | }; 228 | }, 229 | }, 230 | stream: { 231 | resource: function (status, node) { 232 | status.template.resources.Resources[node.id] = { 233 | Type: "AWS::Kinesis::Stream", 234 | Properties: { 235 | ShardCount: 1 236 | } 237 | }; 238 | }, 239 | event: function (status, id, idFrom) { 240 | if (status.model.nodes[id].type === 'fn') { 241 | status.template.functions[id].events.push({ 242 | stream: { 243 | type: "kinesis", 244 | arn: { "Fn::GetAtt": [idFrom, "Arn"] } 245 | } 246 | }); 247 | } else { 248 | status.template.resources.Resources[id].Properties.Events['Stream' + idFrom] = { 249 | Type: "Kinesis", 250 | Properties: { 251 | Stream: { "Fn::GetAtt": [idFrom, "Arn"] }, 252 | StartingPosition: "TRIM_HORIZON", 253 | BatchSize: 10 254 | } 255 | }; 256 | } 257 | }, 258 | policy: function (status, id, idTo) { 259 | return { 260 | Effect: "Allow", 261 | Action: ["kinesis:PutRecord", "kinesis:PutRecords"], 262 | Resource: { "Fn::GetAtt": [idTo, "Arn"] } 263 | }; 264 | }, 265 | }, 266 | deliveryStream: { 267 | resource: function (status, node) { 268 | var targetBucketId = null; 269 | var targetFnId = null; 270 | node.to.forEach(function (idTo) { // Target resources 271 | var node_to = status.model.nodes[idTo]; 272 | if (node_to.type === 'bucket') { 273 | targetBucketId = idTo; 274 | } else if (node_to.type === 'fn') { 275 | targetFnId = idTo; 276 | } 277 | }); 278 | if (targetBucketId == null) { 279 | console.error("Delivery Stream without a destination"); 280 | return; 281 | } 282 | var deliveryPolicyId = node.id + "DeliveryPolicy"; 283 | var deliveryRoleId = node.id + "DeliveryRole"; 284 | // Create the Delivery Strem 285 | status.template.resources.Resources[node.id] = { 286 | DependsOn: [deliveryPolicyId], 287 | Type: 'AWS::KinesisFirehose::DeliveryStream', 288 | Properties: { 289 | ExtendedS3DestinationConfiguration: { 290 | BucketARN: { "Fn::GetAtt": [targetBucketId, "Arn"] }, 291 | BufferingHints: { 292 | IntervalInSeconds: 60, 293 | SizeInMBs: 50 294 | }, 295 | CompressionFormat: "UNCOMPRESSED", 296 | Prefix: "firehose/", 297 | RoleARN: { "Fn::GetAtt": [deliveryRoleId, "Arn"] } 298 | } 299 | } 300 | }; 301 | if (targetFnId !== null) { 302 | status.template.resources.Resources[node.id].Properties 303 | .ExtendedS3DestinationConfiguration.ProcessingConfiguration = { 304 | Enabled: true, 305 | Processors: [{ 306 | Parameters: [{ 307 | ParameterName: "LambdaArn", 308 | ParameterValue: { Ref: `${targetFnId}LambdaFunction` }, 309 | }], 310 | Type: "Lambda" 311 | }] 312 | }; 313 | } 314 | // Create a delivery role 315 | status.template.resources.Resources[deliveryRoleId] = { 316 | Type: 'AWS::IAM::Role', 317 | Properties: { 318 | AssumeRolePolicyDocument: { 319 | Version: '2012-10-17', 320 | Statement: [{ 321 | Effect: "Allow", 322 | Principal: { Service: "firehose.amazonaws.com" }, 323 | Action: 'sts:AssumeRole', 324 | Condition: { 325 | StringEquals: { 326 | 'sts:ExternalId': { Ref: "AWS::AccountId" } 327 | } 328 | } 329 | }] 330 | } 331 | } 332 | }; 333 | // Create a delivery policy for the role 334 | status.template.resources.Resources[deliveryPolicyId] = { 335 | Type: 'AWS::IAM::Policy', 336 | Properties: { 337 | PolicyName: "firehose_delivery_policy", 338 | PolicyDocument: { 339 | Version: '2012-10-17', 340 | Statement: [{ 341 | Effect: 'Allow', 342 | Action: [ 343 | 's3:AbortMultipartUpload', 344 | 's3:GetBucketLocation', 345 | 's3:GetObject', 346 | 's3:ListBucket', 347 | 's3:ListBucketMultipartUploads', 348 | 's3:PutObject' 349 | ], 350 | Resource: { 351 | "Fn::GetAtt": [targetBucketId, "Arn"] 352 | } 353 | }] 354 | }, 355 | Roles: [{ Ref: deliveryRoleId }] 356 | } 357 | }; 358 | }, 359 | event: function () { }, // TODO 360 | policy: function (status, id, idTo) { 361 | return { 362 | Effect: "Allow", 363 | Action: [ 364 | "firehose:PutRecord", 365 | "firehose:PutRecordBatch" 366 | ], 367 | // Kinesis Firehose ARN syntax (can't use GetAtt) 368 | // arn:aws:firehose:region:account-id:deliverystream/delivery-stream-name 369 | Resource: { 370 | "Fn::Join": [ 371 | "", 372 | [ 373 | "arn:aws:firehose:", 374 | { Ref: "AWS::Region" }, 375 | ":", 376 | { Ref: "AWS::AccountId" }, 377 | `:deliverystream/${idTo}` 378 | ] 379 | ] 380 | } 381 | }; 382 | }, 383 | }, 384 | analyticsStream: { 385 | resource: function (status, node) { 386 | 387 | // Input resources 388 | var inputStreamId = null; 389 | var inputDeliveryStreamId = null; 390 | node.from.forEach(function (idFrom) { 391 | var node_from = status.model.nodes[idFrom]; 392 | if (node_from.type === 'stream') { 393 | inputStreamId = idFrom; 394 | } else if (node_from.type === 'deliveryStream') { 395 | inputDeliveryStreamId = idFrom; 396 | } 397 | }); 398 | 399 | // Output resource 400 | var outputStreamId = null; 401 | var outputDeliveryStreamId = null; 402 | node.to.forEach(function (idTo) { 403 | var node_to = status.model.nodes[idTo]; 404 | if (node_to.type === 'stream') { 405 | outputStreamId = idTo; 406 | } else if (node_to.type === 'deliveryStream') { 407 | outputDeliveryStreamId = idTo; 408 | } 409 | }); 410 | 411 | var analyticsStreamRoleId = node.id + "Role"; 412 | var analyticsStreamOutputId = node.id + "Outputs"; 413 | 414 | status.template.resources.Resources[node.id] = { 415 | Type: "AWS::KinesisAnalytics::Application", 416 | Properties: { 417 | ApplicationName: node.id, 418 | Inputs: [{ 419 | NamePrefix: "exampleNamePrefix", 420 | InputSchema: { 421 | RecordColumns: [{ 422 | Name: "example", 423 | SqlType: "VARCHAR(16)", 424 | Mapping: "$.example" 425 | }], 426 | RecordFormat: { 427 | RecordFormatType: "JSON", 428 | MappingParameters: { 429 | JSONMappingParameters: { 430 | RecordRowPath: "$" 431 | } 432 | } 433 | } 434 | } 435 | }] 436 | } 437 | }; 438 | 439 | if (node.description !== '') { 440 | status.template.resources.Resources[node.id] 441 | .Properties.ApplicationDescription = node.description; 442 | } 443 | 444 | if (inputStreamId !== null) { 445 | status.template.resources.Resources[node.id] 446 | .Properties.Inputs[0].KinesisStreamsInput = { 447 | ResourceARN: { "Fn::GetAtt": [inputStreamId, "Arn"] }, 448 | RoleARN: { "Fn::GetAtt": [analyticsStreamRoleId, "Arn"] } 449 | }; 450 | } 451 | 452 | if (inputDeliveryStreamId !== null) { 453 | status.template.resources.Resources[node.id] 454 | .Properties.Inputs[0].KinesisFirehoseInput = { 455 | // Kinesis Firehose ARN syntax (can't use GetAtt) 456 | // arn:aws:firehose:region:account-id:deliverystream/delivery-stream-name 457 | ResourceARN: { 458 | "Fn::Join": [ 459 | "", 460 | [ 461 | "arn:aws:firehose:", 462 | { Ref: "AWS::Region" }, 463 | ":", 464 | { Ref: "AWS::AccountId" }, 465 | `:deliverystream/${inputDeliveryStreamId}` 466 | ] 467 | ] 468 | }, 469 | RoleARN: { "Fn::GetAtt": [analyticsStreamRoleId, "Arn"] } 470 | }; 471 | } 472 | 473 | status.template.resources.Resources[analyticsStreamRoleId] = { 474 | Type: "AWS::IAM::Role", 475 | Properties: { 476 | AssumeRolePolicyDocument: { 477 | Version: "2012-10-17", 478 | Statement: [{ 479 | Effect: "Allow", 480 | Principal: { 481 | Service: "kinesisanalytics.amazonaws.com" 482 | }, 483 | Action: "sts:AssumeRole" 484 | }] 485 | }, 486 | Path: "/", 487 | Policies: [{ 488 | PolicyName: "Open", 489 | PolicyDocument: { 490 | Version: "2012-10-17", 491 | Statement: [{ 492 | Effect: "Allow", 493 | Action: "*", 494 | Resource: "*" 495 | }] 496 | } 497 | }] 498 | } 499 | }; 500 | 501 | status.template.resources.Resources[analyticsStreamOutputId] = { 502 | Type: "AWS::KinesisAnalytics::ApplicationOutput", 503 | DependsOn: node.id, 504 | Properties: { 505 | ApplicationName: { Ref: node.id }, 506 | Output: { 507 | Name: "exampleOutput", 508 | DestinationSchema: { 509 | RecordFormatType: "CSV" 510 | } 511 | } 512 | } 513 | }; 514 | 515 | if (outputStreamId !== null) { 516 | status.template.resources.Resources[analyticsStreamOutputId] 517 | .Properties.Output.KinesisStreamsOutput = { 518 | ResourceARN: { "Fn::GetAtt": [outputStreamId, "Arn"] }, 519 | RoleARN: { "Fn::GetAtt": [analyticsStreamRoleId, "Arn"] } 520 | }; 521 | } 522 | 523 | if (outputDeliveryStreamId !== null) { 524 | status.template.resources.Resources[analyticsStreamOutputId] 525 | .Properties.Output.KinesisFirehoseOutput = { 526 | // Kinesis Firehose ARN syntax (can't use GetAtt) 527 | // arn:aws:firehose:region:account-id:deliverystream/delivery-stream-name 528 | ResourceARN: { 529 | "Fn::Join": [ 530 | "", 531 | [ 532 | "arn:aws:firehose:", 533 | { Ref: "AWS::Region" }, 534 | ":", 535 | { Ref: "AWS::AccountId" }, 536 | `:deliverystream/${outputDeliveryStreamId}` 537 | ] 538 | ] 539 | }, 540 | RoleARN: { "Fn::GetAtt": [analyticsStreamRoleId, "Arn"] } 541 | }; 542 | } 543 | 544 | }, 545 | event: function () { }, // TODO 546 | policy: function () { } // TODO 547 | }, 548 | schedule: { 549 | resource: function (status, node) { 550 | // Nothing to do 551 | }, 552 | event: function (status, id, idFrom) { 553 | if (status.model.nodes[id].type === 'fn') { 554 | status.template.functions[id].events.push({ 555 | schedule: "rate(5 minutes)" 556 | }); 557 | } else { 558 | status.template.resources.Resources[id].Properties.Events['Schedule' + idFrom] = { 559 | Type: "Schedule", 560 | Properties: { 561 | Schedule: "rate(5 minutes)" 562 | } 563 | }; 564 | } 565 | }, 566 | policy: function () { } // This has no sense 567 | }, 568 | topic: { 569 | resource: function (status, node) { 570 | status.template.resources.Resources[node.id] = { 571 | Type: "AWS::SNS::Topic" 572 | }; 573 | }, 574 | event: function (status, id, idFrom) { 575 | if (status.model.nodes[id].type === 'fn') { 576 | status.template.functions[id].events.push({ 577 | sns: idFrom 578 | }); 579 | } else { 580 | status.template.resources.Resources[id].Properties.Events['Topic' + idFrom] = { 581 | Type: "SNS", 582 | Properties: { 583 | Topic: { Ref: idFrom } 584 | } 585 | }; 586 | } 587 | }, 588 | policy: function (status, id, idTo) { 589 | return { 590 | Effect: "Allow", 591 | Action: "sns:Publish", 592 | Resource: { Ref: idTo } // For an SNS topic, it returns the ARN 593 | }; 594 | }, 595 | }, 596 | fn: { 597 | resource: function (status, node) { 598 | // Check for and build a .gitignore if we haven't already 599 | if (!status.files[".gitignore"]) { 600 | status.files[".gitignore"] = runtimes[status.runtime].gitignore; 601 | } 602 | status.template.functions[node.id] = { 603 | handler: node.id + "." + runtimes[status.runtime].handler 604 | }; 605 | if (node.description !== '') { 606 | status.template.functions[node.id].description = node.description; 607 | } 608 | status.files[node.id + '.' + runtimes[status.runtime].fileExtension] = 609 | runtimes[status.runtime].startingCode; 610 | 611 | if (node.from.length > 0) { // There are triggers for this function 612 | status.template.functions[node.id].events = []; 613 | node.from.forEach(function (idFrom) { 614 | console.log("Trigger " + idFrom + " -> " + node.id); 615 | renderingRules[status.model.nodes[idFrom].type].event(status, node.id, idFrom); 616 | }); 617 | } 618 | if (node.to.length > 0) { // There are resources target of this function 619 | var policy = { 620 | Version: "2012-10-17", 621 | Statement: [] 622 | }; 623 | node.to.forEach(function (idTo) { 624 | console.log("Policy " + node.id + " -> " + idTo); 625 | policy.Statement.push( 626 | renderingRules[status.model.nodes[idTo].type] 627 | .policy(status, node.id, idTo) 628 | ); 629 | }); 630 | 631 | if (status.model.nodes[node.id].type !== 'fn') { 632 | status.template.resources.Resources[node.id].Properties.Policies.push(policy); 633 | } 634 | } 635 | }, 636 | event: function () { }, // Nothing to do, this is not a trigger, but a fn to fn invocation 637 | policy: function (status, id, idTo) { 638 | return { 639 | Effect: "Allow", 640 | Action: ["lambda:Invoke", "lambda:InvokeAsync"], 641 | Resource: { "Fn::GetAtt": [idTo, "Arn"] } 642 | }; 643 | } 644 | }, 645 | stepFn: { 646 | resource: function (status, node) { 647 | status.template.resources.Resources[node.id] = { 648 | Type: "AWS::StepFunctions::StateMachine", 649 | Properties: { 650 | // The DefinitionString is added later 651 | // This role is automatically created by the AWS console 652 | // the first time you create a state machine in a region 653 | RoleArn: { 654 | "Fn::Join": [ 655 | "", 656 | [ 657 | "arn:aws:iam::", 658 | { Ref: "AWS::AccountId" }, 659 | ":role/service-role/StatesExecutionRole-", 660 | { Ref: "AWS::Region" } 661 | ] 662 | ] 663 | }, 664 | }, 665 | }; 666 | var definitionString = { 667 | Comment: "A Hello World example", 668 | StartAt: "HelloWorld", 669 | States: { 670 | HelloWorld: { 671 | Type: "Pass", 672 | Result: "Hello World!", 673 | End: true 674 | } 675 | } 676 | }; 677 | // The DefinitionString must be a string with JSON syntax within the template 678 | status.template.resources.Resources[node.id].Properties.DefinitionString = 679 | JSON.stringify(definitionString, null, 2); 680 | }, 681 | event: function () { }, // Nothing to do 682 | policy: function (status, id, idTo) { 683 | return { 684 | "Effect": "Allow", 685 | "Action": [ 686 | "states:DescribeExecution", 687 | "states:GetExecutionHistory", 688 | "states:ListExecutions", 689 | "states:StartExecution", 690 | "states:StopExecution" 691 | ], 692 | "Resource": [ 693 | { Ref: idTo } 694 | ] 695 | }; 696 | } 697 | }, 698 | cognitoIdentity: { 699 | resource: function (status, node) { 700 | var cognitoUnauthRoleId = node.id + "CognitoUnauthRole"; 701 | var cognitoUnauthPolicyId = node.id + "CognitoUnauthPolicy"; 702 | status.template.resources.Resources[node.id] = { 703 | Type: "AWS::Cognito::IdentityPool", 704 | Properties: { 705 | AllowUnauthenticatedIdentities: true // TODO Maybe this is not a secure default ??? 706 | } 707 | }; 708 | status.template.resources.Resources[cognitoUnauthRoleId] = { 709 | Type: 'AWS::IAM::Role', 710 | Properties: { 711 | AssumeRolePolicyDocument: { 712 | Version: '2012-10-17', 713 | Statement: [{ 714 | Effect: "Allow", 715 | Principal: { Federated: "cognito-identity.amazonaws.com" }, 716 | Action: 'sts:AssumeRoleWithWebIdentity', 717 | Condition: { 718 | StringEquals: { 719 | "cognito-identity.amazonaws.com:aud": { Ref: node.id } 720 | }, 721 | "ForAnyValue:StringLike": { 722 | "cognito-identity.amazonaws.com:amr": "unauthenticated" 723 | } 724 | } 725 | }] 726 | } 727 | } 728 | }; 729 | // Create a delivery policy for the role 730 | status.template.resources.Resources[cognitoUnauthPolicyId] = { 731 | Type: 'AWS::IAM::Policy', 732 | Properties: { 733 | PolicyName: "cognito_unauth_policy", 734 | PolicyDocument: { 735 | Version: '2012-10-17', 736 | Statement: [] 737 | }, 738 | Roles: [ { Ref: cognitoUnauthRoleId } ] 739 | } 740 | }; 741 | // Output resources 742 | node.to.forEach(function (idTo) { 743 | // not used??? var node_to = status.model.nodes[idTo]; 744 | status.template.resources.Resources[cognitoUnauthPolicyId] 745 | .Properties.PolicyDocument.Statement.push( 746 | renderingRules[status.model.nodes[idTo].type] 747 | .policy(status, node.id, idTo) 748 | ); 749 | }); 750 | }, 751 | event: function () { }, // TODO ??? 752 | policy: function () { } // TODO ??? 753 | }, 754 | iotRule: { 755 | resource: function (status, node) { 756 | status.template.resources.Resources[node.id] = { 757 | Type: "AWS::IoT::TopicRule", 758 | Properties: { 759 | TopicRulePayload: { 760 | RuleDisabled: "true", // safe choice 761 | Sql: "Select temp FROM 'Some/Topic' WHERE temp > 60", 762 | Actions: [] 763 | } 764 | } 765 | }; 766 | if (node.description !== '') { 767 | status.template.resources.Resources[node.id].Properties.TopicRulePayload.Description = node.description; 768 | } 769 | // Output resources 770 | node.to.forEach(function (idTo) { 771 | var node_to = status.model.nodes[idTo]; 772 | switch (node_to.type) { 773 | case 'fn': 774 | status.template.resources.Resources[node.id].Properties.TopicRulePayload.Actions.push({ 775 | Lambda: { 776 | FunctionArn: `${idTo}LambdaFunction` 777 | } 778 | }); 779 | break; 780 | case 'iotRule': // republish 781 | var republishRoleId = idTo + "PublishRole"; 782 | status.template.resources.Resources[node.id].Properties.TopicRulePayload.Actions.push({ 783 | Republish: { 784 | Topic: "Output/Topic", 785 | RoleArn: { "Fn::GetAtt": [republishRoleId, "Arn"] } 786 | } 787 | }); 788 | status.template.resources.Resources[republishRoleId] = { 789 | Type: "AWS::IAM::Role", 790 | Properties: { 791 | AssumeRolePolicyDocument: { 792 | Version: "2012-10-17", 793 | Statement: [{ 794 | Effect: "Allow", 795 | Action: [ "sts:AssumeRole" ], 796 | Principal: { 797 | Service: [ "iot.amazonaws.com" ] 798 | } 799 | }] 800 | }, 801 | Policies: [{ 802 | PolicyName: "publish", 803 | PolicyDocument: { 804 | Version: "2012-10-17", 805 | Statement: [{ 806 | Effect: "Allow", 807 | Action: "iot:Publish", 808 | Resource: { 809 | "Fn::Join": [ 810 | "", 811 | [ 812 | "arn:aws:iot:", 813 | { Ref: "AWS::Region" }, 814 | ":", 815 | { Ref: "AWS::AccountId" }, 816 | ":topic/Outpu/*" 817 | ] 818 | ] 819 | } 820 | }] 821 | } 822 | }] 823 | } 824 | }; 825 | break; 826 | default: 827 | throw "Error: connection type not supported (" + node_to.type + ")"; 828 | } 829 | }); 830 | }, 831 | event: function () { }, 832 | policy: function () { } 833 | } 834 | }; 835 | 836 | function render(model, runtime, deployment) { 837 | console.log('Using Serverless Framework...'); 838 | var files = {}; 839 | var template = { 840 | service: "serverless", 841 | provider: { 842 | name: "aws", 843 | runtime: runtime 844 | }, 845 | functions: { }, 846 | resources: { 847 | Resources: { }, 848 | Outputs: { } 849 | } 850 | }; 851 | 852 | var status = { 853 | model: model, 854 | runtime: runtime, 855 | files: files, 856 | template: template 857 | }; 858 | 859 | for (var id in model.nodes) { 860 | var node = model.nodes[id]; 861 | renderingRules[node.type].resource(status, node); 862 | } 863 | 864 | console.log(template); // Still in JSON 865 | console.log(JSON.stringify(template, null, 4)); // JSON -> text 866 | 867 | for (var r in template.Resources) { 868 | console.log(r + " -> YAML"); 869 | console.log(jsyaml.safeDump(template.Resources[r], { lineWidth: 1024 })); 870 | } 871 | 872 | // Line breaks can introduce YAML syntax (e.g. >-) that will put some variables 873 | // (e.g. AWS::Region) between quotes. 874 | // Single quotes must be removed for functions (e.g. Fn::GetAtt) to work. 875 | files['serverless.yml'] = jsyaml.safeDump(template, { lineWidth: 1024 }).replace(/'(!.+)'/g, "$1"); 876 | 877 | return files; 878 | } 879 | 880 | module.exports = render; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import $ from 'jquery'; 4 | //window.jQuery = $; 5 | //window.$ = $; 6 | 7 | import 'bootstrap'; 8 | import 'bootstrap/dist/css/bootstrap.min.css'; 9 | 10 | import { Network } from 'vis/index-network'; 11 | import { DataSet } from 'vis/index-timeline-graph2d'; 12 | import 'vis/dist/vis-network.min.css'; 13 | 14 | var JSZip = require("jszip"); 15 | import { saveAs } from 'file-saver'; 16 | 17 | const sam = require('./engines/sam'); 18 | const servfrmwk = require('./engines/servfrmwk'); 19 | 20 | // Engines to build the application in different formats (AWS SAM, ...) 21 | const engines = { 22 | sam: sam, 23 | servfrmwk: servfrmwk 24 | }; 25 | 26 | const enginesTips = { 27 | sam: `You can now build this application using the AWS SAM CLI: 28 | 29 | sam package --s3-bucket --output-template-file packaged.json 30 | 31 | sam deploy --template-file packaged.json --stack-name --capabilities CAPABILITY_IAM 32 | `, 33 | servfrmwk: `You can now build this application using the Serverless Framework: 34 | 35 | serverless deploy` 36 | }; 37 | 38 | const deploymentPreferenceTypes = { 39 | '': 'None', 40 | Canary10Percent5Minutes: 'Canary 10% for 5\'', 41 | Canary10Percent10Minutes: 'Canary 10% for 10\'', 42 | Canary10Percent15Minutes: 'Canary 10% for 15\'', 43 | Canary10Percent30Minutes: 'Canary 10% for 30\'', 44 | Linear10PercentEvery1Minute: 'Linear 10% every 1\'', 45 | Linear10PercentEvery2Minutes: 'Linear 10% every 2\'', 46 | Linear10PercentEvery3Minutes: 'Linear 10% every 3\'', 47 | Linear10PercentEvery10Minutes: 'Linear 10% every 10\'', 48 | AllAtOnce: 'All at Once' 49 | }; 50 | 51 | const nodeTypes = { 52 | api: { 53 | name: 'API Gateway', 54 | image: './img/aws/Amazon-API-Gateway.png' 55 | }, 56 | cognitoIdentity: { 57 | name: 'Cognito Identity', 58 | image: './img/aws/Amazon-Cognito.png' 59 | }, 60 | table: { 61 | name: 'DynamoDB Table', 62 | image: './img/aws/Amazon-DynamoDB_Table.png' 63 | }, 64 | analyticsStream: { 65 | name: 'Kinesis Data Analytics', 66 | image: './img/aws/Amazon-Kinesis-Data-Analytics.png' 67 | }, 68 | deliveryStream: { 69 | name: 'Kinesis Data Firehose', 70 | image: './img/aws/Amazon-Kinesis-Data-Firehose.png' 71 | }, 72 | stream: { 73 | name: 'Kinesis Data Stream', 74 | image: './img/aws/Amazon-Kinesis-Data-Streams.png' 75 | }, 76 | iotRule: { 77 | name: 'IoT Topic Rule', 78 | image: './img/aws/IoT_Rule.png' 79 | }, 80 | fn: { 81 | name: 'Lambda Function', 82 | image: './img/aws/AWS-Lambda_Function.png' 83 | }, 84 | bucket: { 85 | name: 'S3 Bucket', 86 | image: './img/aws/Amazon-S3_Bucket.png' 87 | }, 88 | schedule: { 89 | name: 'Schedule', 90 | image: './img/aws/Amazon-CloudWatch_Event-Time-Based.png' 91 | }, 92 | topic: { 93 | name: 'SNS Topic', 94 | image: './img/aws/Amazon-SNS_Topic.png' 95 | }, 96 | stepFn: { 97 | name: 'Step Function', 98 | image: './img/aws/AWS-Step-Functions.png' 99 | }, 100 | }; 101 | 102 | const nodeConnections = { 103 | bucket: { 104 | topic: { action: 'notification' }, 105 | fn: { action: 'trigger' } 106 | }, 107 | table: { 108 | fn: { action: 'stream' } 109 | }, 110 | api: { 111 | fn: { action: 'integration' }, 112 | stepFn: { action: 'integration' } 113 | }, 114 | stream: { 115 | fn: { action: 'trigger' }, 116 | analyticsStream: { action: 'input' }, 117 | deliveryStream: { action: 'deliver' } 118 | }, 119 | deliveryStream: { 120 | bucket: { action: 'destination' }, 121 | fn: { action: 'transform' } // To transform data in the stream 122 | }, 123 | analyticsStream: { 124 | stream: { action: 'output' }, 125 | deliveryStream: { action: 'output' } 126 | }, 127 | schedule: { 128 | deliveryStream: { action: 'target' }, 129 | stream: { action: 'target' }, 130 | topic: { action: 'target' }, 131 | fn: { action: 'target' } 132 | }, 133 | topic: { 134 | fn: { action: 'trigger' } 135 | }, 136 | fn: { 137 | bucket: { action: 'read/write' }, 138 | table: { action: 'read/write' }, 139 | api: { action: 'invoke' }, 140 | stream: { action: 'put' }, 141 | deliveryStream: { action: 'put' }, 142 | topic: { action: 'notification' }, 143 | fn: { action: 'invoke' }, 144 | stepFn: { action: 'activity' } 145 | }, 146 | stepFn: { 147 | fn: { action: 'invoke' }, 148 | }, 149 | cognitoIdentity: { 150 | fn: { action: 'authorize' }, 151 | api: { action: 'authorize' } 152 | }, 153 | iotRule: { 154 | fn: { action: 'invoke' }, 155 | iotRule: { action: 'republish' } 156 | // These connections require an external/service role 157 | // stream: { action: 'put' }, 158 | // deliveryStream: { action: 'put' }, 159 | // table: { action: 'write' }, 160 | // topic: { action: 'notification' }, 161 | } 162 | }; 163 | 164 | function getUrlParams() { 165 | let p = {}; 166 | let match, 167 | pl = /\+/g, // Regex for replacing addition symbol with a space 168 | search = /([^&=]+)=?([^&]*)/g, 169 | decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); }, 170 | query = window.location.search.substring(1); 171 | while (match = search.exec(query)) 172 | p[decode(match[1])] = decode(match[2]); 173 | return p; 174 | } 175 | 176 | function setSelectOptions(id, options, message) { 177 | let $el = $("#" + id); 178 | $el.empty(); // remove old options 179 | $el.append($("