Workflow an abstraction to define a set actions and tasks.
Task an abstraction to logically group one or more action, for example, init,test.
Action an abstraction defining a call to a service. An action does actual job, like starting service, building and deploying app etc,
ActionRequest an abstraction representing a service request.
ActionResponse an abstraction representing a service response.
To execute action:
- workflow service looks up a service by id, in workflow manager registry.
- workflow service creates a new request for corresponding action on the selected service.
- Action.Request is expanded with context.State ($variable substitution) and converted as service request struct.
- Service executes operation for provided request.
Service an abstraction providing set of functionalities triggered by specified action/request.
State key/value pair map that is used to mange state during the workflow run. The state can be change by providing variable definition. The workflow content, data structures, can use dollar '$' sign followed by variable name to get its expanded to its corresponding state value if the key has been present.
Inline Workflow
For simple sequential tasks, workflow can be defined inline with pipeline run request.
i. e.
@data.yaml
defaults:
datastore: db1
pipeline:
register:
action: dsunit:register
datastore: db1
config:
driverName: postgres
descriptor: host=127.0.0.1 port=5432 user=[username] password=[password] dbname=[dbname]
sslmode=disable
credentials: $pgCredentials
parameters:
dbname: db1
prepare:
mapping:
action: dsunit.mapping
mappings:
- URL: regression/db1/mapping.json
post:
tables: $Tables
sequence:
action: dsunit.sequence
tables: $tables
post:
- seq = $Sequences
data:
action: nop
init:
- key = data.db.setup
- dbSetup = $AsTableRecords($key)
setup:
action: dsunit:prepare
URL: regression/db1/data/
data: $dbSetup
Neatly
For more complex workflow endly uses Neatly format to represent a workflow. In that case neatly is responsible for converting a tabular document (.csv) with json/yaml and other sub path dependencies into workflow object tree as shown in the diagram.
Find out more about neatly: Neatly introduction
To see neatly converted workflow *model.Workflow run the following
i.e. @regression.csv
| Workflow | Name | Description | Tasks | Init | |||||
|---|---|---|---|---|---|---|---|---|---|
| regression | $app app regresion test | %Tasks | @var/init | ||||||
| []Tasks | Name | Description | Actions | ||||||
| prepare | prepare data for test use cases | %Prepare | |||||||
| []Prepare | Service | Action | Description | Request | |||||
| workflow | run | init selenium | @req/selenium_init | ||||||
| workflow | run | init test data | @req/data | ||||||
| []Tasks | Name | Description | Actions | ||||||
| test | Defines test requests | %Test | |||||||
| []Test{1..002} | Subpath | Service | Action | Description | Request | Skip | When | Init | /Data.db.[]setup |
| use_cases/${index}* | nop | skip this group if skip.txt is present | {} |
|
@var/test_init | @use_case.txt | |||
| use_cases/${index}* | nop | push test data | {} |
|
@db1_data | ||||
| use_cases/${index}* | selenium | run | run selenium test | @selenium_test | |||||
| use_cases/${index}* | http/runner | send | run HTTP test | @http_test | |||||
| use_cases/${index}* | rest/runner | send | run REST test | @rest_test | |||||
| []Tasks | Name | Description | Actions | ||||||
| clean | stop test services | %Clean | |||||||
| []Clean | Service | Action | Description | Request | SleepTimeMs | ||||
| nop | sleep for easy debuging | {} | 1000 | ||||||
| run | close and stop seleniun | @req/selenium_destroy |
Printing workflow model representation
endly -w=WORKFLOW_NAME -p -f=yaml|json
For sake of illustrating data flow, let assume p1 and p2 parameters are supplied to workflow. These can be accessed within workflow or its tasks or actions vi the following:
- $params.p1
- $params.p2
A test workflow can be invoked by one of the following methods:
- Command line:
endly -w=test p1=val1 p2=val2- Single workflow run request
endly -r=run@run.yaml
Name: test
Params:
p1: val1
p2: val2- Inline workflow run request:
endly -r=run p2=val2@run.yaml
params:
p1: val1
pipeline:
task1:
action: print
message: $params.p1 $params.p2
task2:
workflow: test
p1: $params.p1
p2: $params.p2 Workflow process uses context.State() to maintain execution state.
Variables an abstraction having capabilities to change a workflow state.
A workflow variable defines data transition between input and output state map.
Variable has the following attributes
-
Name: name can be defined as key to be stored in state map or expression
- array element push ->, for instance ->collection, where collection is a key in the state map
- reference $ for example $ref, where ref is the key in the state, in this case the value will be
-
Value: any type value that is used when from value is empty
-
From name of a key state key, or expression with key.
The following expression are supported:- number increments ++, for example counter++, where counter is a key in the state
- array element shift <-, for example <-collection, where collection is a key in the state
- reference $ for example $ref, where ref is the key in the state, in this case the value will be evaluated as value stored in key pointed by content of ref variable
Variable in actions:
| Operation | Variable.Name | Variable.Value | Variable.From | Input State Before | Input State After | Out State Before | Out State After |
|---|---|---|---|---|---|---|---|
| Assignment | key1 | [1,2,3] | n/a | n/a | n/a | { } | {"key1":[1,2,3]} |
| Assignment by reference | $key1 | 1 | n/a | {"key1":"a"} | n/a | { } | {"a":1} |
| Assignment | key1 | n/a | params.k1 | {"params":{"k1":100}} | n/a | { } | {"key1":100} |
| Assignment by reference | key1 | n/a | $k | {"k":"a", "a":100} | n/a | { } | {"key1":100} |
| Push | ->key1 | 1 | n/a | n/a | n/a | { } | {"key1":[1]} |
| Push | ->key1 | 2 | n/a | n/a | n/a | {"key1":[1]} | {"key1":[1,2]} |
| Shift | item | n/a | <-key1 | n/a | n/a | {"key1":[1, 2]} | {"key1":[2], "item":1} |
| Pre increment | key | n/a | ++i | {"i":100} | {"i":101} | {} | {"key":101} } |
| Post increment | key | n/a | i++ | {"i":100} | {"i":101} | {} | {"key":100} } |
By default, workflow run all specified task, and subtask with sync actions sequentially. All async action are executed independently, task completes when all actions execution is completed.
Each action can control its execution with
Action level criteria control
Each action has the following fields supports conditional expression to control workflow execution
- When: criteria to check if an action is eligible to run
- Skip: criteria to check if the whole group of actions by TagID can be skipped, continuing execution to next group
- Repeater control
type Repeater struct {
Extracts Extracts //textual regexp based data extraction
Variables Variables //structure data based data extraction
Repeat int //how many time send this request
SleepTimeMs int //Sleep time after request send, this only makes sense with repeat option
Exit string //Repeat exit criteria, it uses extracted variable to determine repeat termination
}Workflow goto task action Workflow goto action terminates current task actions execution to start specified current workflow task.`
Workflow switch action Workflow switch action enables to branch execution based on specified context.state key value. Note that switch does not terminate next actions within current task.
Error handling If there is an error during workflow execution, it fails immediately unless OnErrorTask is defined to catch and handle an error. In addition, error key is placed into the config with the following content:
type WorkflowError struct {
Error string
WorkflowName string
TaskName string
Activity *WorkflowServiceActivity
}Finally Workflow also offers DeferTask to execute as the last workflow step in case there is an error or not, for instance, to clean up a resource.
- New context with a new state map is created after inheriting values from a caller. (Caller will not see any state changes from downstream workflow)
- data key is published to the context state with defined workflow.data. Workflow data field would stores complex nested data structure like a setup data.
- params key is published to state map with the caller parameters
- Workflow initialization stage executes, applying variables defined in Workflow.Pre (input: workflow state, output: workflow state)
- Tasks Execution
-
Task eligibility determination:
- If specified tasks are '*' or empty, all task defined in the workflow will run sequentially, otherwise only specified
- Evaluate When if specified
-
Task initialization stage executes, applying variables defined in Task.Pre (input: workflow state, output: workflow state)
-
Executes all eligible actions:
- Action eligibility determination:
- Evaluate When if specified, or Skip for all the actions within the same neatly TagID (tag + Group + Index + Subpath)
- Action initialization stage executes, applying variables defined in Action.Pre (input: workflow state, output: workflow state)
- Executing action on specified service
- Action post stage executes applying variables defined in Action.Post (input: action.response, output: workflow state)
- Action eligibility determination:
-
Task post stage executes, applying variables defined in Task.Post (input: state, output: state)
-
- Workflow post stage executes, applying variables defined in Workflow.Post (input: workflow state, output: workflow.response)
- Context state comes with the following build-in/reserved keys:
- rand - random int64
- date - current date formatted as yyyy-MM-dd
- time - current time formatted as yyyy-MM-dd hh:mm:ss
- ts - current timestamp formatted as yyyyMMddhhmmSSS
- timestamp.yesterday - timestamp in ms
- timestamp.now - timestamp in ms
- timestamp.tomorrow - timestamp in ms
- tmpDir - temp directory
- uuid.next - generate unique id
- uuid.get - returns previously generated unique id, or generate new *.env.XXX where XXX is the Id of the env variable to return
- previous - http previous request used for multi request send
- all UFD registered functions
- Neatly UDF
- AsTableRecords udf converting []*DsUnitTableData into map[string][]map[string]interface{} (used by prepare/expect dsunit service), as table record udf provide sequencing and random id generation functionality for supplied data .
- Delegate a new workflow request to dedicated req/ folder
- Variables in Init, Post should only define state, delegate all variables to var/ folder
- Flag variable as Required or provide a fallback Value
- Use Tag Iterators to group similar class of the tests
- Since JSON inside a tabular cell is not too elegant, try to use Virtual object instead.
- Organize sequential simple tasks into pipeline.
- Organize functionally cohesive complex tasks into workflows.
Here is an example directory layout.
endly
|- manager.csv or run.yaml
|- system.yaml
|- app.yaml
|- datastore.yaml
|
|- system /
| - regression /
| | - regression.csv
| | - var/init.json (workflow init variables)
| | - <use_case_group1> / 1 ... 00X (Tag Iterator)/ <test assets>
| |
| | - <use_case_groupN> / 1 ... 00Y (Tag Iterator)/ <test assets>
| - config /
|
| - datastore /
| - dictionary /
| - schema.ddl
