├── .gitignore ├── LICENSE ├── README.md ├── bin └── processus-cli ├── docs ├── images │ └── processus.png └── user-guide.md ├── engine ├── api.js ├── cli.js ├── envParser.js ├── logger.js ├── persistence │ ├── config.js │ ├── file.js │ ├── mongo.js │ └── store.js ├── processus.js └── title.js ├── package.json ├── runtests.sh ├── taskhandlers ├── conditionHandler.js ├── execHandler.js ├── expectHandler.js ├── fileHandler.js ├── logHandler.js ├── requestHandler.js ├── testHandler.js └── workflowHandler.js ├── test ├── a-json-file.json ├── a-non-json-file.txt ├── background.yml ├── demo1.json ├── demo1.yml ├── demo10.json ├── demo10.yml ├── demo11-async-task.json ├── demo11-async-task.yml ├── demo11.json ├── demo11.yml ├── demo12.json ├── demo12.yml ├── demo13.json ├── demo13.yml ├── demo13a.json ├── demo13a.yml ├── demo14.json ├── demo14.yml ├── demo15.json ├── demo15.yml ├── demo17.json ├── demo17.yml ├── demo18.json ├── demo18.yml ├── demo19.json ├── demo19.yml ├── demo2.json ├── demo2.yml ├── demo3.json ├── demo3.yml ├── demo3a.json ├── demo3a.yml ├── demo4.json ├── demo4.yml ├── demo5.json ├── demo5.yml ├── demo6.json ├── demo6.yml ├── demo7.json ├── demo7.yml ├── demo8.json ├── demo8.yml ├── demo8a.json ├── demo8a.yml ├── demo9.json ├── demo9.yml ├── demoDelete.json ├── demoDelete.yml ├── ex1.json ├── ex1.yml ├── ex2.json ├── ex2.yml ├── expectations.json ├── expectations.yml ├── jshint.json ├── jshint.yml ├── send-slack-message.yml ├── test-all.yml ├── test-api.js ├── test-demo1.yml ├── test-demo10.yml ├── test-demo11-async.yml ├── test-demo11.yml ├── test-demo12.yml ├── test-demo13.yml ├── test-demo13a.yml ├── test-demo14.yml ├── test-demo15.yml ├── test-demo17.yml ├── test-demo19.yml ├── test-demo2.yml ├── test-demo3.yml ├── test-demo4.yml ├── test-demo5.yml ├── test-demo6.yml ├── test-demo7.yml ├── test-demo8.yml └── test-demo9.yml └── wercker.yml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | _data 3 | *.log 4 | .env 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Processus 4 | 5 | Processus is a simple, nodejs based, workflow engine designed to help orchestrate multiple tasks. 6 | 7 | [![Node version](https://img.shields.io/badge/node-v5.0.0-green.svg)](https://nodejs.org/en/) 8 | [![NPM version](https://img.shields.io/npm/v/processus.svg?style=flat-square)](https://www.npmjs.com/package/processus) 9 | [![License MIT](https://img.shields.io/badge/license-MPL-blue.svg)](https://github.com/cloudb2/processus/blob/master/LICENSE) 10 | [![wercker status](https://app.wercker.com/status/08b060f7ea4965ecdbc3389df29d816d/s "wercker status")](https://app.wercker.com/project/bykey/08b060f7ea4965ecdbc3389df29d816d) 11 | 12 | There are many workflow engines, but Processus makes some very specific assumptions that make it easy to quickly write simple, yet powerful workflows. 13 | 14 | * [Installation](#installation) 15 | * [Overview](#overview) 16 | * [Features](#features) 17 | * [Workflow](#workflow) 18 | * [Tasks](#tasks) 19 | * [User Guide](http://cloudb2.github.io/processus/) 20 | * [Contributing](#contributing) 21 | 22 |
23 | 24 | # Getting Started 25 | 26 | ## Installation 27 | 28 | Install using npm within your project 29 | ``` 30 | npm install --save processus 31 | ``` 32 | 33 | Install globally for use on the command line 34 | ``` 35 | npm install -g processus 36 | ``` 37 | 38 | or clone this repo 39 | ``` 40 | git clone https://github.com/cloudb2/processus 41 | cd processus 42 | npm install 43 | ``` 44 | 45 | ### Usage CLI 46 | ``` 47 | $ ./bin/processus-cli -h 48 | 49 | ____ ____ __ ___ ____ ____ ____ _ _ ____ 50 | ( _ \( _ \ / \ / __)( __)/ ___)/ ___)/ )( \/ ___) 51 | ) __/ ) /( O )( (__ ) _) \___ \___ \) \/ (\___ \ 52 | (__) (__\_) \__/ \___)(____)(____/(____/\____/(____/ 53 | 54 | Processus: A Simple Workflow Engine. 55 | 56 | Usage: 57 | processus-cli [OPTIONS] [ARGS] 58 | 59 | Options: 60 | -l, --log [STRING] Sets the log level 61 | [debug|verbose|info|warn|error]. (Default is error) 62 | -f, --file STRING Workflow or task definition. A task must also include 63 | the workflow ID. For YAML use .yml postfix. 64 | -i, --id STRING Workflow ID. 65 | -r, --rewind NUMBER time in reverse chronological order. 0 is current, 1 66 | is the previous save point etc. 67 | -d, --delete STRING delete a workflow instance 68 | --deleteALL delete ALL workflow instances. 69 | -h, --help Display help and usage details 70 | ``` 71 | 72 | ### Usage API 73 | 74 | ``` 75 | var processus = require('processus'); 76 | var store = require('processus/engine/persistence/store'); 77 | 78 | //Initialize the processus store 79 | store.initStore(function(err){ 80 | console.log(err); 81 | }); 82 | 83 | var wf = { 84 | "name": "Example Workflow", 85 | "description": "An example workflow using the API.", 86 | "tasks":{ 87 | "task 1": { 88 | "description": "Demo task to execute echo command.", 89 | "blocking": true, 90 | "handler" : "../taskhandlers/execHandler", 91 | "parameters": { 92 | "cmd": "echo 'Congratulations you called a workflow using the API.'" 93 | } 94 | } 95 | } 96 | }; 97 | 98 | processus.execute(wf, function(err, workflow){ 99 | if(!err) { 100 | console.log(workflow.tasks['task 1'].parameters.stdout); 101 | } 102 | else { 103 | console.log(err); 104 | } 105 | }); 106 | ``` 107 | 108 | which should result in the following: 109 | 110 | ``` 111 | info: ⧖ Starting task [task 1] 112 | info: Congratulations you called a workflow using the API. 113 | info: ✔ task task 1 completed successfully. 114 | Congratulations you called a workflow using the API. 115 | 116 | ``` 117 | 118 | ## Overview 119 | 120 | ### Features 121 | 122 | 1. Define workflow in JSON or YAML 123 | 2. Execute tasks in series (sequentially) or parallel 124 | 3. Nested tasks 125 | 4. Reference data between tasks 126 | 5. Reference data from workflow to tasks 127 | 6. Call a workflow from a workflow 128 | 7. Extensible task handlers 129 | 8. Task handlers included 130 | * testHandler: testing Processus workflows 131 | * execHandler: executing local commands (background tasks now also supported) 132 | * workflowHandler: for calling other workflows 133 | * requestHandler: making HTTP requests 134 | * conditionHandler: evaluating conditional statements 135 | * expectHandler: testing assertions with expect 136 | 9. Built in persistence (file based) 137 | 10. Inject workflow with additional tasks 138 | 11. Pre and Post workflow tasks 139 | 12. Support for environment variables 140 | 13. Inspect executed workflows and look back through their history 141 | 14. Update in-flight (paused) workflows with async callbacks 142 | 15. [Dockerized API](https://github.com/cloudb2/processus-api) 143 | 144 | ### Workflow 145 | 146 | A workflow in Processus is defined using JSON (or equivalent YAML), which should conform to a specific structure. The best way to understand that structure is by looking at examples. 147 | 148 | A workflow, in it's simplest form, is defined as follows. 149 | ``` 150 | { 151 | "tasks": { 152 | }, 153 | "id": "[instance UUID]" 154 | "status": "[open|error|completed]" 155 | } 156 | ``` 157 | Both ```id``` and ```status``` are added by Processus at execution time. 158 | 159 | execute the above example ex1.json using the following command. 160 | ``` 161 | ../bin/processus-cli -l info -f ./test/ex1.json 162 | ``` 163 | 164 | You should see something like this. 165 | ``` 166 | $ ./bin/processus-cli -l info -f ./test/ex1.json 167 | 168 | ____ ____ __ ___ ____ ____ ____ _ _ ____ 169 | ( _ \( _ \ / \ / __)( __)/ ___)/ ___)/ )( \/ ___) 170 | ) __/ ) /( O )( (__ ) _) \___ \___ \) \/ (\___ \ 171 | (__) (__\_) \__/ \___)(____)(____/(____/\____/(____/ 172 | 173 | Processus: A Simple Workflow Engine. 174 | 175 | info: reading workflow file [./test/ex1.json] 176 | info: ✰ Workflow [./test/ex1.json] with id [5e4993b8-563f-448e-a983-3f1e0b342d60] exited without error, but did not complete. 177 | ``` 178 | ***Note*** 179 | 180 | 1. You can add additional meta data to the workflow such as a name and description, but that will be ignored by Processus. 181 | 2. The status of a workflow can be open, error or completed. 182 | 3. In this example there are no tasks, so the Processus returns open, assuming that a task will be injected later. More on this later. 183 | 184 | execute ex1 again, this time with a log level of debug. 185 | ``` 186 | ../bin/processus-cli -l debug -f ./test/ex1.json 187 | ``` 188 | 189 | You should see something like this. 190 | ``` 191 | $ ./bin/processus-cli -l debug -f ./test/ex1.json 192 | 193 | ____ ____ __ ___ ____ ____ ____ _ _ ____ 194 | ( _ \( _ \ / \ / __)( __)/ ___)/ ___)/ )( \/ ___) 195 | ) __/ ) /( O )( (__ ) _) \___ \___ \) \/ (\___ \ 196 | (__) (__\_) \__/ \___)(____)(____/(____/\____/(____/ 197 | 198 | Processus: A Simple Workflow Engine. 199 | 200 | info: reading workflow file [./test/ex1.json] 201 | debug: checking for data directory [_data] 202 | debug: init complete without error. 203 | debug: save point a reached. 204 | debug: save point c reached. 205 | debug: Workflow returned successfully. 206 | debug: { 207 | "tasks": {}, 208 | "status": "open", 209 | "id": "bec87e05-d4c4-43e8-b16c-8c89215f28a2" 210 | } 211 | info: ✰ Workflow [./test/ex1.json] with id [bec87e05-d4c4-43e8-b16c-8c89215f28a2] exited without error, but did not complete. 212 | ``` 213 | ***Note*** 214 | 215 | 1. The status and id have been added by Processus 216 | 2. The workflow remains open as there are NO tasks to execute 217 | 218 | ### Tasks 219 | 220 | Consider the following workflow. 221 | ``` 222 | { 223 | "tasks": { 224 | "say hello": { 225 | "blocking": true, 226 | "handler": "../taskhandlers/execHandler", 227 | "parameters": { 228 | "cmd": "echo 'hello, world'" 229 | } 230 | }, 231 | "say hello again": { 232 | "blocking": true, 233 | "handler": "../taskhandlers/execHandler", 234 | "parameters": { 235 | "cmd": "echo 'hello, world again'" 236 | } 237 | } 238 | } 239 | } 240 | ``` 241 | ***Note*** 242 | 243 | 1. The above workflow has 2 tasks ```say hello``` and ```say hello again```. 244 | 2. Each task uses a handler called ```execHandler``` which executed the command identified in the data property of the task by ```parameters.cmd```. 245 | 3. **See .yml versions in the test directory for a YAML equivalent workflows.** e.g. 246 | ``` 247 | --- 248 | tasks: 249 | say hello: 250 | blocking: true 251 | handler: "../taskhandlers/execHandler" 252 | parameters: 253 | cmd: "echo 'hello, world'" 254 | say hello again: 255 | blocking: true 256 | handler: "../taskhandlers/execHandler" 257 | parameters: 258 | cmd: "echo 'hello, world again'" 259 | ``` 260 | 261 | 262 | So, in short, this simple workflow will execute ```echo 'hello, world'``` and ```echo 'hello, world again'``` sequentially. 263 | 264 | execute ex2.json 265 | ``` 266 | ./bin/processus-cli -l debug -f ./test/ex2.json 267 | ``` 268 | 269 | You should see something like this. 270 | ``` 271 | $ ./bin/processus-cli -l debug -f ./test/ex2.json 272 | 273 | ____ ____ __ ___ ____ ____ ____ _ _ ____ 274 | ( _ \( _ \ / \ / __)( __)/ ___)/ ___)/ )( \/ ___) 275 | ) __/ ) /( O )( (__ ) _) \___ \___ \) \/ (\___ \ 276 | (__) (__\_) \__/ \___)(____)(____/(____/\____/(____/ 277 | 278 | Processus: A Simple Workflow Engine. 279 | 280 | info: reading workflow file [./test/ex2.json] 281 | debug: checking for data directory [_data] 282 | debug: init complete without error. 283 | debug: save point a reached. 284 | debug: task.skipIf = undefined 285 | debug: task.errorIf = undefined 286 | info: ⧖ Starting task [say hello] 287 | debug: stdout ➜ [hello, world 288 | ] 289 | info: ✔ task [say hello] completed successfully. 290 | debug: save point a reached. 291 | debug: task.skipIf = undefined 292 | debug: task.errorIf = undefined 293 | info: ⧖ Starting task [say hello again] 294 | debug: stdout ➜ [hello, world again 295 | ] 296 | info: ✔ task [say hello again] completed successfully. 297 | debug: save point a reached. 298 | debug: save point c reached. 299 | debug: Workflow returned successfully. 300 | debug: { 301 | "tasks": { 302 | "say hello": { 303 | "blocking": true, 304 | "handler": "../taskhandlers/execHandler", 305 | "parameters": { 306 | "cmd": "echo 'hello, world'", 307 | "stdout": "hello, world\n", 308 | "stderr": "" 309 | }, 310 | "status": "completed", 311 | "timeOpened": 1447974872204, 312 | "timeStarted": 1447974872206, 313 | "timeCompleted": 1447974872224, 314 | "handlerDuration": 18, 315 | "totalDuration": 20 316 | }, 317 | "say hello again": { 318 | "blocking": true, 319 | "handler": "../taskhandlers/execHandler", 320 | "parameters": { 321 | "cmd": "echo 'hello, world again'", 322 | "stdout": "hello, world again\n", 323 | "stderr": "" 324 | }, 325 | "status": "completed", 326 | "timeOpened": 1447974872225, 327 | "timeStarted": 1447974872226, 328 | "timeCompleted": 1447974872235, 329 | "handlerDuration": 9, 330 | "totalDuration": 10 331 | } 332 | }, 333 | "status": "completed", 334 | "id": "e83de778-d64b-403f-b29d-c305c9f854dd" 335 | } 336 | info: ✰ Workflow [./test/ex2.json] with id [e83de778-d64b-403f-b29d-c305c9f854dd] completed successfully. 337 | ``` 338 | ***Note*** 339 | 340 | 1. The handler has added ```stdout``` and ```stderr``` to each task's ```parameters``` property. 341 | 2. The status of each task and the overall workflow is shown as ```completed``` 342 | 3. Processus has added additional timing information to each task. 343 | 4. The status of a task can be one of the following 344 | * ```waiting``` It is waiting to be opened by Processus 345 | * ```open``` It is opened by Processus 346 | * ```executing``` The handler associated with this task is currently executing 347 | * ```completed``` The task has completed successfully 348 | * ```paused``` A handler has finished executing but a response is paused. i.e. it is expected that the workflow will be updated at some point in the future from an async callback. 349 | * ```error``` An error occured during execution of the handler 350 | 351 | See the [User Guide](http://cloudb2.github.io/processus/) for much more! 352 | 353 | ## Contributing 354 | 355 | Yes, please. 356 | 357 | Make a pull requests and ensure you can run ```./runtests.sh``` successfully. Please add additional tests for any new features/mods you make. 358 | 359 | ### Roadmap 360 | * Workflow Persistence Plugin Architecture 361 | * Add Mongodb persistence type 362 | * Full REST API to interact with Processus 363 | * Swagger yaml 364 | -------------------------------------------------------------------------------- /bin/processus-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../engine/cli')(); 4 | -------------------------------------------------------------------------------- /docs/images/processus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudb2/processus/ded9f8278073ab44759507a6b1cb4208f062b7a5/docs/images/processus.png -------------------------------------------------------------------------------- /engine/api.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Processus, by cloudb2, MPL2.0 (See LICENSE file in root dir) 3 | * 4 | * api.js: Proessus API as used by other node apps 5 | */ 6 | 7 | //declare required modules 8 | var logger = require('./logger'); 9 | var store = require('./persistence/store'); 10 | var p = require('./processus'); 11 | 12 | //set default log level 13 | logger.level = 'info'; 14 | 15 | module.exports = { 16 | execute: execute, 17 | updateWorkflow: updateWorkflow, 18 | setLogLevel: setLogLevel, 19 | getWorkflow: getWorkflow, 20 | deleteWorkflow: deleteWorkflow, 21 | deleteAll: deleteAll, 22 | getWorkflows: getWorkflows, 23 | saveDefinition: saveDefinition, 24 | getDefinition: getDefinition, 25 | deleteDefinition: deleteDefinition, 26 | init: init, 27 | close: close 28 | }; 29 | 30 | function close(callback){ 31 | store.exitStore(callback); 32 | } 33 | 34 | /** 35 | * Saves the worklfow definition 36 | * @param workflowDef The workflow definition you wish to save. 37 | * @param callback A function(err, workflowDef) 38 | */ 39 | function saveDefinition(workflowDef, callback){ 40 | store.saveDefinition(workflowDef, callback); 41 | } 42 | 43 | /** 44 | * Deletes the worklfow definition 45 | * @param name The name of the workflow definition you wish to delete. 46 | * @param callback A function(err) 47 | */ 48 | function deleteDefinition(name, callback){ 49 | store.deleteDefinition(name, callback); 50 | } 51 | 52 | /** 53 | * Gets the worklfow definition 54 | * @param name The name of the workflow definition you wish to retrieve. 55 | * @param callback A function(err, workflowDef) 56 | */ 57 | function getDefinition(name, callback){ 58 | store.getDefinition(name, callback); 59 | } 60 | 61 | /** 62 | * executes the supplied workflow calls back with the resulting workflow instance. 63 | * @param workflow The workflow you wish to execute. 64 | * @param callback A function(err, workflow) 65 | */ 66 | function execute(workflow, callback){ 67 | p.execute(workflow, callback); 68 | } 69 | 70 | /** 71 | * updates an existing workflow with the supplied tasks. i.e. When a an already 72 | * instantiated workflow has a task in status paused, this function as a callback 73 | * for any async endpoint wishing to respond. 74 | * @param workflowId The UUID of the instantiated workflow 75 | * @param tasks The updated task(s) to be 'injected' into the instantiated workflow 76 | * @param callback A function(err, workflow) 77 | */ 78 | function updateWorkflow(workflowId, tasks, callback){ 79 | p.updateTasks(workflowId, tasks, callback); 80 | } 81 | 82 | /** 83 | * Sets the log level of the Proessus logger. Default is 'error' 84 | * @param level The level [debug|verbose|info|warn|error] 85 | */ 86 | function setLogLevel(level){ 87 | logger.level = level; 88 | return logger; 89 | } 90 | 91 | /** 92 | * Gets an existing instance of a workflow 93 | * @param workflowId The UUID of the instantiated workflow to get 94 | * @param rewind through the history of a workflow. i.e. number from 0 last save 95 | * point, 1 previous save point etc. in continuing reverse chronological order. 96 | * @param callback A function(err, workflow) 97 | */ 98 | function getWorkflow(workflowId, rewind, callback){ 99 | logger.debug("getWorkflow called"); 100 | store.loadInstance(workflowId, rewind, callback); 101 | } 102 | 103 | /** 104 | * Gets a existing instances of a workflows idenified by query 105 | * @param query object representing workflows to search for 106 | * @param callback A function(err, workflow[]) 107 | */ 108 | function getWorkflows(query, callback){ 109 | store.getWorkflows(query, callback); 110 | } 111 | 112 | /** 113 | * Delete an existing instance of a workflow 114 | * @param workflowId The UUID of the instantiated workflow to delete 115 | * @param callback A function(err) 116 | */ 117 | function deleteWorkflow(workflowId, callback) { 118 | store.deleteInstance(workflowId, callback); 119 | } 120 | 121 | /** 122 | * Initialise Processus store based on the configured environment variables 123 | * DB_TYPE default "file" [file | mongo] 124 | * DATA_DIR default "_data" [file only] 125 | * DATA_HOST default "localhost" [mongo only] 126 | * DATA_PORT default 27017 [mongo only] 127 | * @param callback A function(err) 128 | */ 129 | function init(callback){ 130 | store.initStore(callback); 131 | } 132 | 133 | /** 134 | * Deletes ALL workflow instances 135 | * @param callback A function(err) 136 | */ 137 | function deleteAll(callback){ 138 | logger.debug("DELETE ALL"); 139 | store.deleteAll(callback); 140 | } 141 | -------------------------------------------------------------------------------- /engine/cli.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Processus, by cloudb2, MPL2.0 (See LICENSE file in root dir) 3 | * 4 | * cli.js: Command line entry point 5 | */ 6 | 7 | 8 | var logger = require('./logger'); 9 | var cli = require('cli'); 10 | var fs = require('fs'); 11 | var processus = require('./processus'); 12 | var store = require('./persistence/store'); 13 | 14 | //Just export this function 15 | module.exports = function() { 16 | 17 | //Show title, how doesn't like ASCII art? 18 | console.log(require('./title')); 19 | 20 | //Parse command line 21 | cli.parse({ 22 | log: ['l', 'Sets the log level [debug|verbose|info|warn|error].', 'string', 'info'], 23 | file: ['f', 'Workflow or task definition. A task must also include the workflow ID. For YAML use .yml postfix.', 'string', null], 24 | id: ['i', 'Workflow ID.', 'string', null], 25 | rewind: ['r', 'time in reverse chronological order. 0 is current, 1 is the previous save point etc.', 'number', 0], 26 | delete: ['d', 'delete a workflow instance', 'string', null], 27 | deleteALL: ['', 'delete ALL workflow instances.'] 28 | }); 29 | 30 | //Now execute main function 31 | cli.main(function(args, options) { 32 | 33 | //check and set log level 34 | if (options.log === 'debug' || 35 | options.log === 'verbose' || 36 | options.log === 'info' || 37 | options.log === 'warn' || 38 | options.log === 'error') { 39 | logger.level = options.log; 40 | } 41 | else { 42 | logger.error("✘ Invalid log level, see help for more info."); 43 | return -1; 44 | } 45 | 46 | //ok log set, lets' init the store and do the rest 47 | store.initStore(function(err){ 48 | 49 | if(err){ 50 | logger.error("Failed to initialise store: " + err.message); 51 | //well that wasn't a good start! goodbye 52 | process.exit(1); 53 | } 54 | 55 | //Command line wants to delete all 56 | if(options.deleteALL === true) { 57 | store.deleteAll(function(err){ 58 | if(err){ 59 | logger.error(err); 60 | process.exit(1); 61 | } 62 | else { 63 | process.exit(0); 64 | } 65 | }); 66 | return; 67 | } 68 | 69 | //Command line wants to delete a specific instance 70 | if(options.delete !== null) { 71 | logger.info("deleting " + options.delete); 72 | store.deleteInstance(options.delete, function(err){ 73 | if(err){ 74 | logger.error(err); 75 | process.exit(1); 76 | } 77 | else { 78 | process.exit(0); 79 | } 80 | }); 81 | return; 82 | } 83 | 84 | //We got this far, did we get a file or an id? 85 | if (options.file === null && options.id === null) { 86 | logger.error("✘ Must supply a worklfow or task filename."); 87 | process.exit(1); 88 | } 89 | 90 | //Command line wants to get an existing instance 91 | if (options.file === null && options.id !== null) { 92 | //just an id supplied, so fetch that workflow 93 | store.loadInstance(options.id, options.rewind, function(err, workflowFile){ 94 | if(!err){ 95 | //force logger to info 96 | logger.level = 'info'; 97 | if(workflowFile !== undefined){ 98 | logger.info(JSON.stringify(workflowFile, null, 2)); 99 | process.exit(0); 100 | return; 101 | } 102 | else { 103 | logger.error("Unable to find workflow instance [" + options.id + "]"); 104 | process.exit(1); 105 | return; 106 | } 107 | } 108 | else { 109 | logger.error(err.message); 110 | process.exit(1); 111 | return; 112 | } 113 | }); 114 | return; 115 | } 116 | 117 | //Ok, got this far, so we must have a file name to load 118 | var workflowTaskJSON; 119 | store.loadDefinition(options.file, function(err, workflowFile){ 120 | if(!err){ 121 | workflowTaskJSON = workflowFile; 122 | } 123 | else { 124 | logger.error(err.message); 125 | return err; 126 | } 127 | }); 128 | 129 | if(workflowTaskJSON === undefined){ 130 | logger.error("Workflow definition [" + options.file + "] not found."); 131 | process.exit(1); 132 | return; 133 | } 134 | 135 | //Right, well, we have the workflow and all looks good, let's execute it 136 | //fingers crossed! 137 | processus.runWorkflow(options.file, options.id, workflowTaskJSON, function(err, workflow){ 138 | if(err){ 139 | logger.error(err.message); 140 | process.exit(1); 141 | } 142 | else { 143 | process.exit(0); 144 | } 145 | }); 146 | }); 147 | }); 148 | }; 149 | -------------------------------------------------------------------------------- /engine/envParser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Processus, by cloudb2, MPL2.0 (See LICENSE file in root dir) 3 | * 4 | * envParser.js: parse workflow for env vars 5 | */ 6 | 7 | /* 8 | deprecated in favor of workflow.environment:{} being referencable 9 | 10 | var expect = require('expect'); 11 | var _ = require('underscore'); 12 | 13 | module.exports = { 14 | parse: parse 15 | }; 16 | 17 | function parse(rawWorkflow){ 18 | str = "" + rawWorkflow; 19 | 20 | //Look for all instances of '$env[]' 21 | envs = str.match(/[$]env(\[(.*?)\])/g); 22 | 23 | if(envs) { 24 | //Cycle through fetching the env value and replacing 25 | for(var x=0; x]' 27 | envKey = envs[x].substring(5, envs[x].length -1); 28 | envValue = process.env[envKey]; 29 | str = str.replace(envs[x], envValue); 30 | } 31 | } 32 | return str; 33 | } 34 | */ 35 | -------------------------------------------------------------------------------- /engine/logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Processus, by cloudb2, MPL2.0 (See LICENSE file in root dir) 3 | * 4 | * logger.js: Processus logger (based on the wonderful winston) 5 | */ 6 | 7 | var logger = require('winston'); 8 | 9 | logger.setLevels({error: 4, 10 | warn: 3, 11 | info: 2, 12 | verbose: 1, 13 | debug: 0}); 14 | logger.addColors({error: 'red', 15 | warn: 'yellow', 16 | info: 'cyan', 17 | verbose: 'magenta', 18 | debug: 'green'}); 19 | 20 | logger.remove(logger.transports.Console); 21 | 22 | logger.add(logger.transports.Console, { level: 'info', 23 | colorize:true, 24 | stderrLevels:['error'] }); 25 | 26 | logger.level = "info"; //default 27 | 28 | module.exports = logger; 29 | -------------------------------------------------------------------------------- /engine/persistence/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Processus, by cloudb2, MPL2.0 (See LICENSE file in root dir) 3 | * 4 | * config.js: used to configure the persistence store 5 | */ 6 | 7 | //Fetch and set default env vars 8 | var type = process.env.DB_TYPE !== undefined ? process.env.DB_TYPE : "file"; 9 | var dataDirectory = process.env.DB_DIR !== undefined ? process.env.DB_DIR : "_data"; 10 | var dataHost = process.env.DB_HOST !== undefined ? process.env.DB_HOST : "localhost"; 11 | var dataPort = process.env.DB_PORT !== undefined ? process.env.DB_PORT : 0; 12 | 13 | //declare module exports 14 | exports.config = { 15 | "type": type, //default "file" 16 | "dataDirectory": dataDirectory, //default "_data", 17 | "host": dataHost, 18 | "port": dataPort 19 | }; 20 | -------------------------------------------------------------------------------- /engine/persistence/file.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Processus, by cloudb2, MPL2.0 (See LICENSE file in root dir) 3 | * 4 | * file.js: used to manage file based persistence store 5 | */ 6 | 7 | //declare required modules 8 | var logger = require('../logger'); 9 | var fs = require('fs'); 10 | //var envParser = require('../envParser'); 11 | var glob = require('glob'); 12 | var yaml = require('js-yaml'); 13 | 14 | var initialised = false; 15 | 16 | //declare module exports 17 | module.exports = { 18 | deleteInstance: deleteInstance, 19 | loadDefinition: loadDefinition, 20 | loadInstance: loadInstance, 21 | initStore: initStore, 22 | saveInstance: saveInstance, 23 | deleteAll: deleteAll, 24 | exitStore: exitStore, 25 | saveDefinition: saveDefinition, 26 | getDefinition: getDefinition, 27 | getWorkflows: getWorkflows, 28 | deleteDefinition: deleteDefinition 29 | }; 30 | 31 | var gConfig; 32 | 33 | function saveDefinition(workflowDef, callback){ 34 | try { 35 | fs.writeFileSync(gConfig.dataDirectory + "/" + workflowDef.name + ".def", JSON.stringify(workflowDef, null, 2)); 36 | callback(null, workflowDef); 37 | } 38 | catch(fileError){ 39 | callback(fileError); 40 | } 41 | } 42 | 43 | function getDefinition(name, callback){ 44 | try { 45 | var workflowDef = fs.readFileSync(gConfig.dataDirectory + "/" + name + ".def", "utf8"); 46 | workflowDef = JSON.parse(workflowDef); 47 | callback(null, workflowDef); 48 | } 49 | catch(fileError){ 50 | callback(fileError); 51 | } 52 | } 53 | 54 | function deleteDefinition(name, callback){ 55 | try { 56 | fs.unlink(gConfig.dataDirectory + "/" + name + ".def", function(err){ 57 | callback(err); 58 | }); 59 | } 60 | catch(fileError){ 61 | callback(fileError); 62 | } 63 | } 64 | 65 | function deleteAll(callback){ 66 | logger.debug("DELETE ALL"); 67 | glob(gConfig.dataDirectory + "/!(*.def)", function (err, files) { 68 | logger.debug("deleting files " + JSON.stringify(files, null, 2)); 69 | if(err){ 70 | callback(err); 71 | return; 72 | } 73 | var delError; 74 | if(files) { 75 | logger.debug("deleting files " + JSON.stringify(files, null, 2)); 76 | 77 | for(var x=0; x 0 ) { 206 | if (rewind > files.length) { 207 | logger.warn("rewind value [" + rewind + "] is before the workflow started, assuming the oldest [" + files.length + "]."); 208 | rewind = files.length; 209 | 210 | } 211 | index = files.length - rewind < files.length ? files.length - rewind : 0; 212 | current = files[index]; 213 | } 214 | 215 | fs.readFile(current, function (err, data) { 216 | var workflowLoaded; 217 | if (err) { 218 | logger.error("✘ Unable to find workflow [" + id + "] " + err); 219 | } 220 | else { 221 | workflowLoaded = JSON.parse(data); 222 | } 223 | callback(err, workflowLoaded); 224 | }); 225 | } 226 | else { 227 | logger.error("✘ Unable to find workflow [" + id + "] " + err); 228 | } 229 | 230 | }); 231 | } 232 | catch(fileError){ 233 | callback(fileError); 234 | } 235 | 236 | 237 | } 238 | 239 | function getWorkflows(query, callback){ 240 | callback(new Error("getWorkflows is not implemented in file type storage, use mongo.")); 241 | } 242 | 243 | function initStore(config, callback){ 244 | 245 | gConfig = config; 246 | 247 | if(!initialised) { 248 | var stat; 249 | 250 | try { 251 | logger.debug("checking for data directory [" + gConfig.dataDirectory + "]"); 252 | stat = fs.statSync(gConfig.dataDirectory); 253 | initialised = true; 254 | } 255 | catch(err) { 256 | try { 257 | logger.debug("creating data directory [" + gConfig.dataDirectory + "]"); 258 | fs.mkdirSync(gConfig.dataDirectory); 259 | initialised = true; 260 | } 261 | catch(error) { 262 | logger.error("✘ Fatal Error, unable to find or create the data directory. " + error); 263 | callback(error); 264 | return; 265 | } 266 | } 267 | } 268 | callback(null); 269 | } 270 | 271 | function saveInstance(workflow, callback) { 272 | var current = gConfig.dataDirectory + "/" + workflow.id; 273 | //If the file already exists rename it based on current time 274 | var stat; 275 | try { 276 | stat = fs.statSync(current); 277 | try { 278 | fs.renameSync(current, current + "_" + Date.now()); 279 | fs.writeFileSync(current, JSON.stringify(workflow, null, 2)); 280 | callback(null); 281 | 282 | } 283 | catch(renameError) { 284 | logger.error("✘ Fatal Error, unable to rename existing workflow. " + renameError); 285 | callback(renameError); 286 | } 287 | } 288 | catch(existsError) { 289 | writeCurrent(workflow, current, function(err){ 290 | callback(err); 291 | }); 292 | } 293 | } 294 | 295 | function writeCurrent(workflow, current, callback) { 296 | //Save current workflow 297 | try { 298 | fs.writeFileSync(current, JSON.stringify(workflow, null, 2)); 299 | callback(null); 300 | } 301 | catch(writeError) { 302 | callback(writeError); 303 | } 304 | 305 | } 306 | 307 | function exitStore(callback) { 308 | callback(null); 309 | } 310 | -------------------------------------------------------------------------------- /engine/persistence/mongo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Processus, by cloudb2, MPL2.0 (See LICENSE file in root dir) 3 | * 4 | * mongo.js: used to manage mongodb based persistence store 5 | */ 6 | 7 | //declare required modules 8 | var logger = require('../logger'); 9 | var MongoClient = require('mongodb').MongoClient; 10 | 11 | //declare module exports 12 | module.exports = { 13 | deleteInstance: deleteInstance, 14 | loadDefinition: loadDefinition, 15 | saveDefinition: saveDefinition, 16 | getDefinition: getDefinition, 17 | loadInstance: loadInstance, 18 | initStore: initStore, 19 | saveInstance: saveInstance, 20 | deleteAll: deleteAll, 21 | exitStore: exitStore, 22 | deleteDefinition: deleteDefinition, 23 | getWorkflows: getWorkflows 24 | }; 25 | 26 | //mongodb and collections, note we resuse these and rely on mongo's pooling 27 | var mongodb; 28 | var workflowInstances; 29 | var workflowHistory; 30 | var workflowDefinitions; 31 | 32 | 33 | function initStore(config, callback){ 34 | try { 35 | var url = "mongodb://" + config.host + ":" + config.port + "/processus"; 36 | // Connect using MongoClient 37 | MongoClient.connect(url, function(err, db) { 38 | 39 | if(!err){ 40 | //store DB and collections for future use 41 | mongodb = db; 42 | workflowInstances = mongodb.collection('instances'); 43 | workflowHistory = mongodb.collection('instances-history'); 44 | workflowDefinitions = mongodb.collection('definitions'); 45 | 46 | //Create index (if not already there) note: ensureIndex is deprecated 47 | db.createIndex('instances', {id:1}, {unique:true, background:true, w:1}, function(err, indexName) { 48 | if(!err) { 49 | db.createIndex('instances-history', {id:1}, {unique:true, background:true, w:1}, function(err, indexName) { 50 | if(!err) { 51 | db.createIndex('definitions', {name:1}, {unique:true, background:true, w:1}, function(err, indexName){ 52 | //All done, passback last error (if any) 53 | callback(err); 54 | }); 55 | } 56 | else { 57 | //failed to create index instances-history 58 | callback(err); 59 | } 60 | }); 61 | } 62 | else { 63 | //failed to create index instances 64 | callback(err); 65 | } 66 | }); 67 | } 68 | else { 69 | //Failed to connect, passback error 70 | callback(err); 71 | } 72 | }); 73 | } 74 | catch(mongoError){ 75 | callback(mongoError); 76 | } 77 | } 78 | 79 | 80 | function deleteAll(callback){ 81 | try { 82 | //delete all instances and history 83 | deleteAllInstances(function(err){ 84 | if(!err){ 85 | deleteAllHistory(function(err){ 86 | callback(err); 87 | }); 88 | } 89 | else { 90 | callback(err); 91 | } 92 | }); 93 | } 94 | catch(mongoError){ 95 | callback(mongoError); 96 | } 97 | } 98 | 99 | function deleteAllInstances(callback){ 100 | try { 101 | //delete all instances of the instances collection 102 | workflowInstances.deleteMany({}, function(err, result) { 103 | callback(err); 104 | }); 105 | } 106 | catch(mongoError){ 107 | callback(mongoError); 108 | } 109 | } 110 | 111 | function deleteAllHistory(callback){ 112 | try { 113 | //delete all instances of the instances-history collection 114 | workflowHistory.deleteMany({}, function(err) { 115 | callback(err); 116 | }); 117 | } 118 | catch(mongoError){ 119 | callback(mongoError); 120 | } 121 | } 122 | 123 | function deleteInstanceHistory(id, callback){ 124 | var query = {"id": new RegExp('^' + id + "_")}; 125 | workflowHistory.deleteMany(query, function(err){ 126 | callback(err); 127 | }); 128 | } 129 | 130 | function deleteInstance(id, callback){ 131 | try { 132 | workflowInstances.deleteOne({"id": id}, function(err) { 133 | if(!err){ 134 | deleteInstanceHistory(id, function(err){ 135 | callback(err); 136 | }); 137 | } 138 | else { 139 | callback(err); 140 | } 141 | 142 | }); 143 | } 144 | catch(mongoError){ 145 | callback(mongoError); 146 | } 147 | } 148 | 149 | function deleteDefinition(name, callback){ 150 | try { 151 | workflowDefinitions.deleteOne({name: name}, function(err){ 152 | callback(err); 153 | }); 154 | } 155 | catch(mongoError){ 156 | callback(mongoError); 157 | } 158 | } 159 | 160 | function loadDefinition(id, callback){ 161 | try { 162 | //defer to file handler 163 | require("./file").loadDefinition(id, function(err, workflowDef){ 164 | callback(err, workflowDef); 165 | /* 166 | if(!err){ 167 | saveDefinition(workflowDef, function(err){ 168 | callback(err, workflowDef); 169 | }); 170 | } 171 | else { 172 | callback(err); 173 | } 174 | */ 175 | }); 176 | 177 | } 178 | catch(fileErr){ 179 | callback(fileErr); 180 | } 181 | 182 | } 183 | 184 | function saveDefinition(workflowDef, callback){ 185 | try { 186 | workflowDefinitions.update({name:workflowDef.name}, workflowDef, {upsert: true},function(err, r) { 187 | callback(err, r); 188 | }); 189 | } 190 | catch(mongoError){ 191 | callback(mongoError); 192 | } 193 | } 194 | 195 | function getDefinition(name, callback){ 196 | try { 197 | workflowDefinitions.findOne({name:name}, function(err, r) { 198 | r._id = undefined; 199 | callback(err, r); 200 | }); 201 | } 202 | catch(mongoError){ 203 | callback(mongoError); 204 | } 205 | } 206 | 207 | function getWorkflows(query, callback){ 208 | try { 209 | workflowInstances.find(query).toArray().then( 210 | function(instances) { 211 | callback(null, instances); 212 | } 213 | ); 214 | 215 | 216 | } 217 | catch(mongoError){ 218 | callback(mongoError); 219 | } 220 | 221 | } 222 | 223 | function loadInstance(id, rewind, callback) { 224 | try { 225 | if(rewind === 0) { 226 | //rewind is current, so get the latest 227 | workflowInstances.findOne({"id": id}, function(err, result) { 228 | logger.debug("mongo found: " + result); 229 | callback(null, result); 230 | }); 231 | } 232 | else { 233 | //Create regex starts with id_ 234 | var query = {"id": new RegExp('^' + id + "_")}; 235 | workflowHistory.find(query).toArray().then( 236 | function(history) { 237 | var index = history.length -1 - rewind; 238 | if(index < 0 )index = 0; 239 | //based on rewind value passback appropriate version of history 240 | callback(null, history[index]); 241 | }); 242 | } 243 | } 244 | catch(mongoError){ 245 | callback(mongoError); 246 | } 247 | } 248 | 249 | 250 | function saveInstance(workflow, callback) { 251 | try { 252 | //if there's no _id (as added by mongo), then insert a new one 253 | if(workflow._id === undefined){ 254 | workflowInstances.insertOne(workflow, function(err, r) { 255 | logger.debug("mongo inserted: " + r); 256 | if(!err){ 257 | var historyWorkflow = JSON.parse(JSON.stringify(workflow)); 258 | historyWorkflow._id = undefined; 259 | historyWorkflow.id = workflow.id + "_" + Date.now(); 260 | workflowHistory.insertOne(historyWorkflow, function(err, r) { 261 | callback(err); 262 | }); 263 | } 264 | else { 265 | callback(err); 266 | } 267 | }); 268 | } 269 | else { 270 | //reparse workflow object before updating, not doing this has caused 271 | //node to exist unexpectedly with Assertion failed: ((object->InternalFieldCount()) > (0)) 272 | var updatedWorkflow = JSON.parse(JSON.stringify(workflow)); 273 | updatedWorkflow._id = workflow._id; 274 | workflowInstances.updateOne({_id: workflow._id}, updatedWorkflow, function(err, r) { 275 | if(!err){ 276 | //now record the history of this save point 277 | var historyWorkflow = JSON.parse(JSON.stringify(workflow)); 278 | historyWorkflow._id = undefined; 279 | historyWorkflow.id = workflow.id + "_" + Date.now(); 280 | workflowHistory.insertOne(historyWorkflow, function(err, r) { 281 | callback(err); 282 | }); 283 | } 284 | else { 285 | callback(err); 286 | } 287 | }); 288 | } 289 | } 290 | catch(mongoError){ 291 | callback(mongoError); 292 | } 293 | } 294 | 295 | function exitStore(callback) { 296 | //if the db client is connected, try and close the DB (it's good housekeeping) 297 | //although node existiting will release the connection and DB. 298 | if(mongodb !== undefined && mongodb !== null){ 299 | mongodb.close(); 300 | } 301 | callback(null); 302 | } 303 | -------------------------------------------------------------------------------- /engine/persistence/store.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Processus, by cloudb2, MPL2.0 (See LICENSE file in root dir) 3 | * 4 | * store.js: persistence store entry point 5 | */ 6 | 7 | //declare required modules 8 | var logger = require('../logger'); 9 | var config = require('./config').config; 10 | var EventEmitter = require('events'); 11 | var deasync = require('deasync'); 12 | 13 | //declare module exports 14 | module.exports = { 15 | deleteInstance: deleteInstance, 16 | loadDefinition: loadDefinition, 17 | loadInstance: loadInstance, 18 | initStore: initStore, 19 | saveInstance: saveInstance, 20 | deleteAll: deleteAll, 21 | exitStore: exitStore, 22 | saveDefinition: saveDefinition, 23 | getDefinition: getDefinition, 24 | getWorkflows: getWorkflows, 25 | deleteDefinition: deleteDefinition 26 | }; 27 | 28 | function deleteAll(callback){ 29 | if(config.type !== null && config.type !== undefined) { 30 | require('./' + config.type).deleteAll(callback); 31 | } 32 | else { 33 | callback(new Error("Persistence store error, no store type selected.")); 34 | } 35 | } 36 | 37 | function deleteInstance(id, callback) { 38 | if(config.type !== null && config.type !== undefined) { 39 | require('./' + config.type).deleteInstance(id, callback); 40 | } 41 | else { 42 | callback(new Error("Persistence store error, no store type selected.")); 43 | } 44 | } 45 | 46 | function getDefinition(name, callback){ 47 | if(config.type !== null && config.type !== undefined) { 48 | require('./' + config.type).getDefinition(name, callback); 49 | } 50 | else { 51 | callback(new Error("Persistence store error, no store type selected.")); 52 | } 53 | } 54 | 55 | function saveDefinition(workflowDef, callback){ 56 | if(config.type !== null && config.type !== undefined) { 57 | require('./' + config.type).saveDefinition(workflowDef, callback); 58 | } 59 | else { 60 | callback(new Error("Persistence store error, no store type selected.")); 61 | } 62 | } 63 | 64 | function deleteDefinition(name, callback){ 65 | if(config.type !== null && config.type !== undefined) { 66 | require('./' + config.type).deleteDefinition(name, callback); 67 | } 68 | else { 69 | callback(new Error("Persistence store error, no store type selected.")); 70 | } 71 | } 72 | 73 | function loadDefinition(id, callback) { 74 | if(config.type !== null && config.type !== undefined) { 75 | require('./' + config.type).loadDefinition(id, callback); 76 | } 77 | else { 78 | callback(new Error("Persistence store error, no store type selected.")); 79 | } 80 | } 81 | 82 | function loadInstance(id, rewind, callback) { 83 | logger.debug("loading instance called with " + id + ", " + rewind); 84 | if(config.type !== null && config.type !== undefined) { 85 | require('./' + config.type).loadInstance(id, rewind, callback); 86 | } 87 | else { 88 | callback(new Error("Persistence store error, no store type selected.")); 89 | } 90 | } 91 | 92 | function getWorkflows(query, callback){ 93 | if(config.type !== null && config.type !== undefined) { 94 | require('./' + config.type).getWorkflows(query, callback); 95 | } 96 | else { 97 | callback(new Error("Persistence store error, no store type selected.")); 98 | } 99 | } 100 | 101 | function initStore(callback) { 102 | try { 103 | if(config.type !== null && config.type !== undefined) { 104 | require('./' + config.type).initStore(config, callback); 105 | } 106 | else { 107 | callback(null); 108 | } 109 | } 110 | catch(storeErr){ 111 | callback(storeErr); 112 | } 113 | } 114 | 115 | function saveInstance(workflow, callback) { 116 | try { 117 | if(config.type !== null && config.type !== undefined) { 118 | require('./' + config.type).saveInstance(workflow, function(err){ 119 | callback(err); 120 | }); 121 | } 122 | else { 123 | callback(null); 124 | } 125 | } 126 | catch(storeErr){ 127 | callback(storeErr); 128 | } 129 | 130 | } 131 | 132 | function exitStore(callback) { 133 | logger.debug("Store is exiting..."); 134 | if(config.type !== null && config.type !== undefined) { 135 | require('./' + config.type).exitStore(callback); 136 | } 137 | else { 138 | callback(null); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /engine/processus.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Processus, by cloudb2, MPL2.0 (See LICENSE file in root dir) 3 | * 4 | * processus.js: The main engine, where the work gets done 5 | */ 6 | 7 | var logger = require('./logger'); 8 | require('dotenv').load({silent: true}); 9 | var async = require("async"); 10 | var uuid = require("node-uuid"); 11 | var store = require('./persistence/store'); 12 | var _ = require("underscore"); 13 | 14 | module.exports = { 15 | execute: execute, 16 | updateTasks: updateTasks, 17 | runWorkflow: runWorkflow 18 | }; 19 | 20 | function runWorkflow(defId, id, workflowTaskJSON, callback) { 21 | if (id === null || id === undefined) { 22 | 23 | execute(workflowTaskJSON, function(err, workflow){ 24 | if(!err) { 25 | logger.debug("Workflow returned successfully."); 26 | logger.debug(JSON.stringify(workflow, null, 2)); 27 | if(workflow.status === "completed"){ 28 | logger.info("✰ Workflow [" + defId + "] with id [" + workflow.id + "] completed successfully."); 29 | } 30 | else { 31 | logger.info("✰ Workflow [" + defId + "] with id [" + workflow.id + "] exited without error, but did not complete."); 32 | } 33 | } 34 | else { 35 | logger.error("✘ " + err.message); 36 | logger.error("✘ Workflow [" + defId + "] with id [" + workflow.id + "] exited with error!"); 37 | logger.debug(JSON.stringify(workflow, null, 2)); 38 | } 39 | callback(err, workflow); 40 | }); 41 | } 42 | 43 | if(id !== null && id !== undefined){ 44 | updateTasks(id, workflowTaskJSON, function(err, workflow){ 45 | if(!err) { 46 | logger.debug("Workflow returned successfully."); 47 | logger.debug(JSON.stringify(workflow, null, 2)); 48 | if(workflow.status === "completed"){ 49 | logger.info("✰ Workflow [" + defId + "] with id [" + id + "] updated successfully."); 50 | } 51 | } 52 | else { 53 | logger.error("✘ " + err.message); 54 | logger.error("✘ Workflow [" + defId + "] with id [" + id + "] failed to update with error!"); 55 | logger.debug(JSON.stringify(workflow, null, 2)); 56 | } 57 | callback(err, workflow); 58 | }); 59 | } 60 | 61 | } 62 | 63 | function updateTasks(id, tasks, callback){ 64 | 65 | store.loadInstance(id, 0, function(err, workflow){ 66 | if(!err){ 67 | if(workflow.status !== "completed") { 68 | workflow = mergeTasks(workflow, tasks); 69 | execute(workflow, callback); 70 | } 71 | else { 72 | callback(new Error("Update failed, workflow [" + id + "] has already completed!")); 73 | } 74 | } 75 | else { 76 | callback(err); 77 | } 78 | }); 79 | } 80 | 81 | function mergeTasks(workflow, tasks) { 82 | function makeTaskHandler(taskName) { 83 | return function taskHandler(task, name) { 84 | if(taskName == name) { 85 | mergeTask(task, tasks[taskName]); 86 | return false; 87 | } 88 | else { 89 | //continue scanning 90 | return true; 91 | } 92 | }; 93 | } 94 | taskNames = Object.keys(tasks); 95 | for(var x=0; x 0) { 328 | logger.debug("found paused task(s) so returning immediately"); 329 | callback(null, workflow); 330 | return; 331 | } 332 | 333 | //Open any waiting (and available) tasks 334 | openNextAvailableTask(workflow); 335 | 336 | //Get a list of ALL the open tasks 337 | var openTasks = getTasksByStatus(workflow, 'open', true); 338 | var taskNames = Object.keys(openTasks); 339 | 340 | //Initialise the task execution queue 341 | var taskExecutionQueue = []; 342 | 343 | //This function will return a function to be used by async that calls the 344 | //appopriate handler (as defined by the task) 345 | function makeTaskExecutionFunction(x){ 346 | return function(callback){ 347 | var taskName = taskNames[x]; 348 | var taskObject = openTasks[taskNames[x]]; 349 | executeTask(workflow.id, taskName, taskObject, callback); 350 | }; 351 | } 352 | 353 | //Now cycle through the open tasks, check them to see if they can be executed, 354 | //and if so, pushed onto the queue 355 | for (var x=0; x 0) { 383 | 384 | //Now execute open tasks 385 | async.parallel(taskExecutionQueue, 386 | //function callback for async when all tasks have finsihed or an error has occured 387 | function(error, results) { 388 | //if no error then cycle through results and update the task statuses 389 | if(!error) { 390 | //ok, all done and no error, so recurse into next set of tasks (if any) 391 | realExecute(workflow, callback); 392 | } 393 | else { 394 | 395 | //Now set the overall workflow to error 396 | workflow.status = 'error'; 397 | store.saveInstance(workflow, function(err){ 398 | logger.debug("save point b reached."); 399 | 400 | if(err){ 401 | callback(err, workflow); 402 | return; 403 | } 404 | else { 405 | callback(error, workflow); 406 | } 407 | }); 408 | } 409 | }); 410 | } 411 | else { 412 | //check if ALL tasks are completed, if so, set the workflow status 413 | if(childHasStatus(workflow, 'completed', true)){ 414 | workflow.status = 'completed'; 415 | } 416 | store.saveInstance(workflow, function(err){ 417 | logger.debug("save point c reached."); 418 | done = true; 419 | //None left in the queue so callback 420 | if(err){ 421 | callback(err, workflow); 422 | return; 423 | } 424 | else { 425 | callback(null, workflow); 426 | } 427 | }); 428 | 429 | } 430 | 431 | }); 432 | 433 | } 434 | 435 | //check data values and look out for $[] references and update the value accordingly 436 | function setTaskDataValues(workflow, task){ 437 | 438 | 439 | var taskProperties = Object.keys(task); 440 | 441 | taskProperties.map(function(propertyKey){ 442 | 443 | var prop = task[propertyKey]; 444 | 445 | //convert whole task to JSON string 446 | var propStr = JSON.stringify(prop, null, 2); 447 | 448 | logger.debug("checking for $[] in " + propStr); 449 | //Now look for matching '$[]' references 450 | refValues = propStr.match(/[$](\[(.*?)\])/g); 451 | 452 | if(refValues) { 453 | 454 | //Cycle through fetching the ref values and replacing 455 | for(var x=0; x=1.0.0", 37 | "deasync": "^0.1.4", 38 | "dotenv": "^1.2.0", 39 | "expect": "^1.13.0", 40 | "glob": "^6.0.1", 41 | "js-yaml": "^3.4.3", 42 | "mongodb": ">=3.1.13", 43 | "node-uuid": "^1.4.3", 44 | "processus-handler-slack": "^0.0.5", 45 | "request": "^2.66.0", 46 | "underscore": "^1.8.3", 47 | "winston": "^1.1.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./bin/processus-cli -f ./test/test-all.yml 3 | -------------------------------------------------------------------------------- /taskhandlers/conditionHandler.js: -------------------------------------------------------------------------------- 1 | /* Condition Handler 2 | * A very simple condition evaluation handler for non programmers 3 | * Task INPUT 4 | * @param task.parameters.conditions Condition objects consisting of 5 | "[condition name]"{ 6 | "valueA":[ValueA], 7 | "operator":[operator], 8 | "valueB":[valueB] 9 | } 10 | * where [operator] must be one of the following: 11 | * "IS", "EQUALS", "=", "MATCH" 12 | * "IS NOT", "NOT EQUALS", "!=", "NOT MATCH", 13 | * "GREATER THAN", ">", 14 | * "LESS THAN", "<", 15 | * "GREATER OR EQUALS", ">=", 16 | * "LESS OR EQUALS", "<=" 17 | * (note case is ignored) 18 | * Task OUTPUT 19 | * @param task.parameters.conditions each condition is updated to include a result e.g. 20 | "[condition name]"{ 21 | "valueA":[ValueA], 22 | "operator":[operator], 23 | "valueB":[valueB], 24 | "valid": [true if condition is valid], 25 | "invalid": [true if condition is invalid] 26 | } 27 | * @param task.parameters.anyValid true if ANY condition evaluates to true 28 | * @param task.parameters.allValid true if ALL conditions evaluate to true 29 | * @param task.parameters.notAnyValid convenience property to show !anyValid 30 | * @param task.parameters.notAllValid convenience property to show !allValid 31 | */ 32 | module.exports = function(workflowId, taskName, task, callback, logger){ 33 | 34 | //validate that task data element exists 35 | if(!task.parameters) { 36 | logger.debug("No task parameters property!"); 37 | callback(new Error("Task [" + taskName + "] has no parameters property!"), task); 38 | return; 39 | } 40 | 41 | //Validate that the data cmd property has been set 42 | if(!task.parameters.conditions) { 43 | callback(new Error("Task [" + taskName + "] has no parameters.conditions property set!"), task); 44 | return; 45 | } 46 | 47 | //get the conditions 48 | conditionNames = Object.keys(task.parameters.conditions); 49 | 50 | task.parameters.andResult = false; 51 | task.parameters.orResult = false; 52 | 53 | if(conditionNames.length > 0) { task.parameters.andResult = true; } 54 | 55 | for(var x=0; x") { 98 | task.parameters.conditions[condition].valid = (valA > valB); 99 | } 100 | else if(op.toLowerCase() === "less than" || 101 | op.toLowerCase() === "less" || 102 | op.toLowerCase() === "<") { 103 | task.parameters.conditions[condition].valid = (valA < valB); 104 | } 105 | else if(op.toLowerCase() === "greater or equals" || 106 | op.toLowerCase() === ">=") { 107 | task.parameters.conditions[condition].valid = (valA >= valB); 108 | } 109 | else if(op.toLowerCase() === "less or equals" || 110 | op.toLowerCase() === "<=") { 111 | task.parameters.conditions[condition].valid = (valA <= valB); 112 | } 113 | else { 114 | callback(new Error("Unknown conditional operator [" + op + "] in task [" + taskName + "]"), task); 115 | return; 116 | } 117 | 118 | task.parameters.conditions[condition].invalid = !task.parameters.conditions[condition].valid; 119 | 120 | //update orResult and andResult 121 | if(task.parameters.conditions[condition].valid === true) { 122 | //at least 1 or more condition is true so set orResult accordingly 123 | task.parameters.anyValid = true; 124 | } 125 | 126 | if(task.parameters.allValid === true && task.parameters.conditions[condition].valid === true){ 127 | task.parameters.allValid = true; 128 | } 129 | else { 130 | task.parameters.allValid = false; 131 | } 132 | } 133 | 134 | task.parameters.notAllValid = !task.parameters.allValid; 135 | task.parameters.notAnyValid = !task.parameters.anyValid; 136 | 137 | callback(null, task); 138 | 139 | }; 140 | -------------------------------------------------------------------------------- /taskhandlers/execHandler.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | var spawn = require('child_process').spawn; 3 | var fs = require('fs'); 4 | 5 | /* Exec Handler 6 | * Using the Task's parameters.cmd property, this handler will attempt to execute that 7 | * command as a child process. To spawn a detatched command use background = true and 8 | * arguments parameters described below. 9 | * output is stored in parameters.stdout and parameters.stderr (unless background = true) 10 | * Task INPUT 11 | * @param task.parameters.cmd The command to execute 12 | * @param task.parameters.background Set true to spawn and detach the process 13 | Note: detached processes will write stdout and stderr to [workflowId].log 14 | * @param task.parameters.arguments Set to an array of arguments 15 | Note: arguments are only required for background (spawned) commands 16 | * Task OUTPUT 17 | * @param task.parameters.stdout The stdout (if any) 18 | * @param task.parameters.stderr The stderr (if any) 19 | * @param task.parameters.pid The child process (if background is true) 20 | * 21 | */ 22 | module.exports = function(workflowId, taskName, task, callback, logger){ 23 | 24 | //validate that task data element exists 25 | if(!task.parameters) { 26 | logger.debug("No task parameters property!"); 27 | callback(new Error("Task [" + taskName + "] has no task parameters property!"), task); 28 | return; 29 | } 30 | 31 | //Validate that the data cmd property has been set 32 | if(!task.parameters.cmd) { 33 | callback(new Error("Task [" + taskName + "] has no parameters.cmd property set!"), task); 34 | return; 35 | } 36 | 37 | if(task.parameters.background === true){ 38 | out = fs.openSync('./' + workflowId + '.log', 'a'); 39 | err = fs.openSync('./' + workflowId + '.log', 'a'); 40 | var child = spawn(task.parameters.cmd, task.parameters.arguments, { 41 | detached: true, 42 | stdio: [ 'ignore', out, err ] 43 | }); 44 | task.parameters.pid = child.pid; 45 | child.unref(); 46 | callback(null, task); 47 | } 48 | else { 49 | //execute the command and check the response 50 | exec(task.parameters.cmd, function(error, stdout, stderr) { 51 | 52 | //Set the stdout and stderr properties of the data object in the task 53 | //strip out last cr/lf 54 | task.parameters.stdout = stdout.replace(/\n$/, ""); 55 | task.parameters.stderr = stderr.replace(/\n$/, ""); 56 | if(task.parameters.stdout !== ""){ logger.info(task.parameters.stdout); } 57 | if(task.parameters.stderr !== ""){ logger.error(task.parameters.stderr);} 58 | if(error){ 59 | callback(new Error("exec failed with: [" + error.message + "] in task ["+ taskName + "]"), task); 60 | return; 61 | } 62 | if(task.parameters.stderr !== ""){ 63 | callback(new Error("exec failed with: [" + stderr + "] in task ["+ taskName + "]"), task); 64 | } 65 | else { 66 | callback(null, task); 67 | } 68 | 69 | }); 70 | } 71 | 72 | }; 73 | -------------------------------------------------------------------------------- /taskhandlers/expectHandler.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect'); 2 | 3 | /* Expect Handler 4 | * A wraper for node expect module 5 | * see https://github.com/mjackson/expect for usage 6 | * supported expect functions are: 7 | * toExist 8 | * toNotExist 9 | * toBe 10 | * toNotBe 11 | * toEqual 12 | * toNotEqual 13 | * toBeA 14 | * toNotBeA 15 | * toMatch 16 | * toBeLessThan 17 | * toBeGreaterThan 18 | * toInclude 19 | * toExclude 20 | * 21 | * Task INPUT 22 | * @param task.parameters.expectations is an object consisting of expects. e.g. 23 | [expect name]{ 24 | "assertion" [expect function] 25 | "object": [object to test], 26 | "value": [value to expect], 27 | "message": [A message to return upon failure] 28 | } 29 | * Task OUTPUT 30 | * if task.ignoreError = false 31 | * Each expect object is furnished with an assertion true or an error 32 | * if task.ignoreError = true 33 | * Each expect object is furnished with an assertion true or false (errors are suppressed) 34 | * 35 | [expect name]{ 36 | "assertion" [expect function] 37 | "object": [object to test], 38 | "value": [value to expect], 39 | "message": [A message to return upon failure] 40 | "assertion": [true | false] 41 | } 42 | */ 43 | module.exports = function(workflowId, taskName, task, callback, logger){ 44 | 45 | //validate that task data element exists 46 | if(!task.parameters) { 47 | logger.debug("No task parameters property!"); 48 | callback(new Error("Task [" + taskName + "] has no task parameters property!"), task); 49 | return; 50 | } 51 | 52 | //Validate that the data cmd property has been set 53 | if(!task.parameters.expectations) { 54 | callback(new Error("Task [" + taskName + "] has no parameters.expectations property set!"), task); 55 | return; 56 | } 57 | 58 | //get the expect names 59 | var expectNames = Object.keys(task.parameters.expectations); 60 | 61 | for(var x=0; x