Skip to content

Commit

Permalink
fix: add final flag on step condition
Browse files Browse the repository at this point in the history
Signed-off-by: Adrien Barreau <adrien.barreau@ovhcloud.com>
  • Loading branch information
deathiop authored and rclsilver committed Aug 29, 2023
1 parent 62fa1bc commit f76bdce
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 7 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,16 +334,23 @@ A step is the smallest unit of work that can be performed within a task. At is's
A sequence of ordered steps constitutes the entire workload of a task. Steps are ordered by declaring **dependencies** between each other. A step declares its dependencies as a list of step names on which it waits, meaning that a step's execution will be on hold until its dependencies have been resolved. [More details about dependencies](#dependencies).

The flow of this sequence can further be controlled with **conditions** on the steps: a condition is a clause that can be run before or after the step's action. A condition can either be used:

- to skip a step altogether
- to analyze its outcome and override the engine's default behaviour

Several conditions can be specified, the first one to evaluate as `true` is applied. A condition is composed of:
Several conditions can be specified.
Unless `final` is set to `true`, they are all evaluated in order. If multiple conditions evaluate to `true`, they will be applied sequentially. Once a condition is applied, the next condition is evaluated using the new context (i.e. using the new `state` value of steps that got updated). If multiple conditions are evaluated to `true` and are changing the same step `state` value, then the last condition to evaluate as `true` will be the one that will change the `state` step _for real_.

A condition is composed of:

- a `type` (skip or check)
- a list of `if` assertions (`value`, `operator`, `expected`) which all have to be true (AND on the collection),
- a `then` object to impact the state of steps (`this` refers to the current step)
- a `final` boolean, defaulting to `false`. When set to `true`, it prevents the evaluation of the next conditions if this one is evaluated to `true`
- an optional `message` to convey the intention of the condition, making it easier to inspect tasks

Here's an example of a `skip` condition. The value of an input is evaluated to determine the result: if the value of `runType` is `dry`, the `createUser` step will not be executed, its state will be set directly to DONE.
Here's an example of a `skip` condition. The value of an input is evaluated to determine the result: if the value of `runType` is `dry`, the `createUser` step will not be executed, its state will be set directly to `DONE`.

```yaml
inputs:
- name: runType
Expand All @@ -365,7 +372,7 @@ steps:
message: Dry run, skip user creation
```

Here's an example of a `check` condition. Here the return of an http call is inspected: a 404 status will put the step in a custom NOT_FOUND state. The default behavior would be to consider any 4xx status as a client error, which blocks execution of the task. The check condition allows you to consider this situation as normal, and proceed with other steps that take the NOT_FOUND state into account (creating the missing resource, for instance).
Here's an example of a `check` condition. Here the return of an http call is inspected: a 404 status will put the step in a custom `NOT_FOUND` state. The default behavior would be to consider any 4xx status as a client error, which blocks execution of the task. The check condition allows you to consider this situation as normal, and proceed with other steps that take the `NOT_FOUND` state into account (creating the missing resource, for instance).

```yaml
steps:
Expand Down
12 changes: 10 additions & 2 deletions engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,24 +455,32 @@ func TestStepConditionStates(t *testing.T) {
assert.NotNil(t, res)
assert.Nil(t, err)
assert.Equal(t, step.StateTODO, res.Steps["stepOne"].State)
assert.Equal(t, step.StateTODO, res.Steps["stepOneFinal"].State)
assert.Equal(t, resolution.StateTODO, res.State)
assert.Equal(t, 0, res.Steps["stepOne"].TryCount)
assert.Equal(t, 0, res.Steps["stepOneFinal"].TryCount)

res, err = runResolution(res)

assert.Nil(t, err)
assert.NotNil(t, res)
assert.Equal(t, step.StateToRetry, res.Steps["stepOne"].State)
assert.Equal(t, resolution.StateError, res.State)
assert.Equal(t, step.StatePrune, res.Steps["stepOneFinal"].State)
assert.Equal(t, step.StateClientError, res.Steps["stepSkip"].State)
assert.Equal(t, step.StatePrune, res.Steps["stepSkipFinal"].State)
assert.Equal(t, resolution.StateBlockedBadRequest, res.State)
assert.Equal(t, 1, res.Steps["stepOne"].TryCount)
assert.Equal(t, 1, res.Steps["stepOneFinal"].TryCount)

res, err = runResolution(res)

assert.Nil(t, err)
assert.NotNil(t, res)
assert.Equal(t, step.StateToRetry, res.Steps["stepOne"].State)
assert.Equal(t, resolution.StateError, res.State)
assert.Equal(t, step.StatePrune, res.Steps["stepOneFinal"].State)
assert.Equal(t, resolution.StateBlockedBadRequest, res.State)
assert.Equal(t, 2, res.Steps["stepOne"].TryCount)
assert.Equal(t, 1, res.Steps["stepOneFinal"].TryCount)

assert.Equal(t, "REGEXP_MATCH", res.Steps["stepTwo"].State)
assert.Equal(t, "NOTREGEXP_MATCH", res.Steps["stepTwoBis"].State)
Expand Down
1 change: 1 addition & 0 deletions engine/step/condition/stepcondition.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Condition struct {
Type string `json:"type"`
If []*Assert `json:"if"`
Then map[string]string `json:"then"`
Final bool `json:"final"`
Message string `json:"message"`
}

Expand Down
17 changes: 16 additions & 1 deletion engine/step/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,15 +504,23 @@ func PreRun(st *Step, values *values.Values, ss StateSetter, executedSteps map[s
break
}
}

// Reaching this means the condition is met: set the step to skipped
st.skipped = true
// inserting current skipped step into executedSteps to avoid being picked-up again in availableSteps candidates

// Inserting current skipped step into executedSteps to avoid being picked-up again in availableSteps candidates
executedSteps[st.Name] = true
for step, state := range sc.Then {
if step == stepRefThis {
step = st.Name
}
ss(step, state, sc.Message)
}

// If the condition is final, don't continue
if sc.Final {
break
}
}
}

Expand Down Expand Up @@ -544,12 +552,19 @@ func AfterRun(st *Step, values *values.Values, ss StateSetter) {
break
}
}

// Reaching this means the condition is met
for step, state := range sc.Then {
if step == stepRefThis {
step = st.Name
}
ss(step, state, sc.Message)
}

// If the condition is final, don't continue
if sc.Final {
break
}
}
}

Expand Down
76 changes: 75 additions & 1 deletion engine/templates_tests/stepCondition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ steps:
then:
this: TO_RETRY
message: Second condition is also true, overrides the impact of the first
stepOneFinal:
description: first step final
action:
type: echo
configuration:
output:
foo: bar
conditions:
- type: check
if:
- value: "{{.step.stepOneFinal.output.foo}}"
operator: EQ
expected: bar
then:
this: PRUNE
final: true
message: First condition evals to true and is applied
- type: check
if:
- value: "{{.step.stepOneFinal.output.foo}}"
operator: NE
expected: grault
then:
this: TO_RETRY
message: Second condition is not run since the first one is final
stepTwo:
description: a regexp condition is applied
custom_states: [REGEXP_MATCH]
Expand Down Expand Up @@ -212,4 +237,53 @@ steps:
output:
{
concat: "hello world",
}
}
stepSkip:
description: skip condition step
action:
type: echo
configuration:
output:
foo: bar
conditions:
- type: skip
if:
- value: 1
operator: EQ
expected: 1
then:
this: PRUNE
message: First condition evals to true and is applied
- type: skip
if:
- value: 1
operator: NE
expected: 2
then:
this: CLIENT_ERROR
message: Second condition is also true, overrides the impact of the first
stepSkipFinal:
description: skip condition step final
action:
type: echo
configuration:
output:
foo: bar
conditions:
- type: skip
if:
- value: 1
operator: EQ
expected: 1
then:
this: PRUNE
final: true
message: First condition evals to true and is applied
- type: skip
if:
- value: 1
operator: NE
expected: 2
then:
this: CLIENT_ERROR
message: Second condition is not run since the first one is final

0 comments on commit f76bdce

Please sign in to comment.