├── .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 | [](https://nodejs.org/en/)
8 | [](https://www.npmjs.com/package/processus)
9 | [](https://github.com/cloudb2/processus/blob/master/LICENSE)
10 | [](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