Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: conditional step execution #3485

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
542 changes: 291 additions & 251 deletions api/v1alpha1/generated.pb.go

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions api/v1alpha1/generated.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions api/v1alpha1/promotion_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ type PromotionStep struct {
Task *PromotionTaskReference `json:"task,omitempty" protobuf:"bytes,5,opt,name=task"`
// As is the alias this step can be referred to as.
As string `json:"as,omitempty" protobuf:"bytes,2,opt,name=as"`
// If is an optional expression that, if present, must evaluate to a boolean
// value. If the expression evaluates to false, the step will be skipped.
If string `json:"if,omitempty" protobuf:"bytes,7,opt,name=if"`
// Retry is the retry policy for this step.
Retry *PromotionStepRetry `json:"retry,omitempty" protobuf:"bytes,4,opt,name=retry"`
// Vars is a list of variables that can be referenced by expressions in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ spec:
expressions in defining values at any level of this block.
See https://docs.kargo.io/references/expression-language for details.
x-kubernetes-preserve-unknown-fields: true
if:
description: |-
If is an optional expression that, if present, must evaluate to a boolean
value. If the expression evaluates to false, the step will be skipped.
type: string
retry:
description: Retry is the retry policy for this step.
properties:
Expand Down
5 changes: 5 additions & 0 deletions charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ spec:
expressions in defining values at any level of this block.
See https://docs.kargo.io/references/expression-language for details.
x-kubernetes-preserve-unknown-fields: true
if:
description: |-
If is an optional expression that, if present, must evaluate to a boolean
value. If the expression evaluates to false, the step will be skipped.
type: string
retry:
description: Retry is the retry policy for this step.
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ spec:
expressions in defining values at any level of this block.
See https://docs.kargo.io/references/expression-language for details.
x-kubernetes-preserve-unknown-fields: true
if:
description: |-
If is an optional expression that, if present, must evaluate to a boolean
value. If the expression evaluates to false, the step will be skipped.
type: string
retry:
description: Retry is the retry policy for this step.
properties:
Expand Down
5 changes: 5 additions & 0 deletions charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ spec:
expressions in defining values at any level of this block.
See https://docs.kargo.io/references/expression-language for details.
x-kubernetes-preserve-unknown-fields: true
if:
description: |-
If is an optional expression that, if present, must evaluate to a boolean
value. If the expression evaluates to false, the step will be skipped.
type: string
retry:
description: Retry is the retry policy for this step.
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,37 @@ steps:
input: ${{ outputs.alias.someOutput }}
```

#### Conditional Steps

You can conditionally execute a step based on the result of a previous step by
using the `if` key in the step definition. The value of the `if` key must be a
valid [expression](40-expressions.md) that evaluates to a boolean value.

```yaml
steps:
- uses: step-name
if: ${{ outputs.step1.someOutput == 'value' }}
```

If the expression evaluates to `true`, the step is executed as normal. If the
expression evaluates to `false`, the step is skipped and the next step in the
sequence is executed.

:::info
In a future release, Kargo will be adding support for improved failure and
error handling, which will supercharge the ability to conditionally execute
steps based on the outcome of previous steps.

Refer to [this issue](https://github.com/akuity/kargo/issues/3228) for more
information and updates.
:::

:::tip
Conditional steps can be used in [Promotion Tasks](20-promotion-tasks.md) to
conditionally execute a task step based on provided
[task variables](20-promotion-tasks.md#task-variables).
:::

#### Step Retries

When a step fails for any reason, it can be retried instead of immediately
Expand Down
1 change: 1 addition & 0 deletions internal/controller/promotions/promotions.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@
steps[i] = directives.PromotionStep{
Kind: step.Uses,
Alias: step.As,
If: step.If,

Check warning on line 473 in internal/controller/promotions/promotions.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/promotions/promotions.go#L473

Added line #L473 was not covered by tests
Retry: step.Retry,
Vars: step.Vars,
Config: step.Config.Raw,
Expand Down
38 changes: 38 additions & 0 deletions internal/directives/promotions.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
// step will be keyed to this alias by the Engine and made accessible to
// subsequent steps.
Alias string
// If is an optional expression that, if present, must evaluate to a boolean
// value. If the expression evaluates to false, the step will be skipped.
If string
// Retry is the retry configuration for the PromotionStep.
Retry *kargoapi.PromotionStepRetry
// Vars is a list of variables definitions that can be used by the
Expand Down Expand Up @@ -200,6 +203,41 @@
return env
}

// Skip returns true if the PromotionStep should be skipped based on the If
// condition. The If condition is evaluated in the context of the provided
// PromotionContext and State.
func (s *PromotionStep) Skip(
promoCtx PromotionContext,
state State,
) (bool, error) {
if s.If == "" {
return false, nil
}

vars, err := s.GetVars(promoCtx, state)
if err != nil {
return false, err
}

Check warning on line 220 in internal/directives/promotions.go

View check run for this annotation

Codecov / codecov/patch

internal/directives/promotions.go#L219-L220

Added lines #L219 - L220 were not covered by tests

env := s.BuildEnv(
promoCtx,
StepEnvWithOutputs(state),
StepEnvWithTaskOutputs(s.Alias, state),
StepEnvWithVars(vars),
)

v, err := expressions.EvaluateTemplate(s.If, env)
if err != nil {
return false, err
}

Check warning on line 232 in internal/directives/promotions.go

View check run for this annotation

Codecov / codecov/patch

internal/directives/promotions.go#L231-L232

Added lines #L231 - L232 were not covered by tests

if b, ok := v.(bool); ok {
return !b, nil
}

return false, fmt.Errorf("expression must evaluate to a boolean")

Check warning on line 238 in internal/directives/promotions.go

View check run for this annotation

Codecov / codecov/patch

internal/directives/promotions.go#L238

Added line #L238 was not covered by tests
}

// GetConfig returns the Config unmarshalled into a map. Any expr-lang
// expressions are evaluated in the context of the provided arguments
// prior to unmarshaling.
Expand Down
71 changes: 71 additions & 0 deletions internal/directives/promotions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,3 +559,74 @@ func TestPromotionStep_GetConfig(t *testing.T) {
})
}
}
func TestPromotionStep_Skip(t *testing.T) {
tests := []struct {
name string
step *PromotionStep
ctx PromotionContext
state State
assertions func(*testing.T, bool, error)
}{
{
name: "no if condition",
step: &PromotionStep{},
assertions: func(t *testing.T, b bool, err error) {
assert.False(t, b)
assert.NoError(t, err)
},
},
{
name: "if condition uses vars",
step: &PromotionStep{
If: "${{ vars.foo == 'bar' }}",
},
ctx: PromotionContext{
Vars: []kargoapi.PromotionVariable{
{
Name: "foo",
Value: "bar",
},
},
},
assertions: func(t *testing.T, b bool, err error) {
assert.NoError(t, err)
assert.False(t, b)
},
},
{
name: "if condition uses outputs",
step: &PromotionStep{
If: "${{ outputs.foo == 'bar' }}",
},
state: State{
"foo": "bar",
},
assertions: func(t *testing.T, b bool, err error) {
assert.NoError(t, err)
assert.False(t, b)
},
},
{
name: "if condition uses task outputs",
step: &PromotionStep{
Alias: "task::other-alias",
If: "${{ task.outputs.alias.foo == 'bar' }}",
},
state: State{
"task::alias": map[string]any{
"foo": "baz",
},
},
assertions: func(t *testing.T, b bool, err error) {
assert.NoError(t, err)
assert.True(t, b)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.step.Skip(tt.ctx, tt.state)
tt.assertions(t, got, err)
})
}
}
29 changes: 26 additions & 3 deletions internal/directives/simple_engine_promote.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@
default:
}

// Prepare the step for execution by setting the alias.
step := steps[i]

// Prepare the step for execution by setting the alias.
if step.Alias, err = e.stepAlias(step.Alias, i); err != nil {
return PromotionResult{
Status: kargoapi.PromotionPhaseErrored,
Expand All @@ -105,13 +106,35 @@
// If we don't have metadata for this step yet, create it.
if int64(len(stepExecMetas)) == i {
stepExecMetas = append(stepExecMetas, kargoapi.StepExecutionMetadata{
Alias: step.Alias,
StartedAt: ptr.To(metav1.Now()),
Alias: step.Alias,
})
}
stepExecMeta := &stepExecMetas[i]

// Check if the step should be skipped.
var skip bool
if skip, err = step.Skip(promoCtx, state); err != nil {
return PromotionResult{
Status: kargoapi.PromotionPhaseErrored,
CurrentStep: i,
StepExecutionMetadata: stepExecMetas,
State: state,
HealthCheckSteps: healthChecks,
}, fmt.Errorf("error checking if step %d should be skipped: %w", i, err)

Check warning on line 123 in internal/directives/simple_engine_promote.go

View check run for this annotation

Codecov / codecov/patch

internal/directives/simple_engine_promote.go#L117-L123

Added lines #L117 - L123 were not covered by tests
} else if skip {
// TODO(hidde): We should probably set the status to skipped here,
// but this would require step specific phases (opposed to the
// promotion wide phases we have now). We should revisit this when
// we dive into the other engine related changes (e.g. improved
// failure handling).
stepExecMeta.Status = kargoapi.PromotionPhaseSucceeded
continue
}

// Execute the step
if stepExecMeta.StartedAt == nil {
stepExecMeta.StartedAt = ptr.To(metav1.Now())
}
result, err := e.executeStep(ctx, promoCtx, step, reg, workDir, state)
stepExecMeta.Status = result.Status
stepExecMeta.Message = result.Message
Expand Down
37 changes: 37 additions & 0 deletions internal/directives/simple_engine_promote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,43 @@ func TestSimpleEngine_executeSteps(t *testing.T) {
}, result.State)
},
},
{
name: "conditional step execution",
steps: []PromotionStep{
{Kind: "success-step", Alias: "step1"},
{Kind: "error-step", Alias: "step2", If: "${{ false }}"},
{Kind: "success-step", Alias: "step3", If: "${{ true }}"},
},
assertions: func(t *testing.T, result PromotionResult, err error) {
assert.NoError(t, err)
assert.Equal(t, kargoapi.PromotionPhaseSucceeded, result.Status)
assert.Equal(t, int64(2), result.CurrentStep)

// Verify the result contains metadata from all steps
assert.Len(t, result.StepExecutionMetadata, 3)
for _, metadata := range result.StepExecutionMetadata {
assert.Equal(t, kargoapi.PromotionPhaseSucceeded, metadata.Status)
switch metadata.Alias {
case "step2":
assert.Nil(t, metadata.StartedAt)
assert.Nil(t, metadata.FinishedAt)
default:
assert.NotNil(t, metadata.StartedAt)
assert.NotNil(t, metadata.FinishedAt)
}
}

// Verify state contains outputs from both steps
assert.Equal(t, State{
"step1": map[string]any{
"key": "value",
},
"step3": map[string]any{
"key": "value",
},
}, result.State)
},
},
{
name: "start from middle step",
promoCtx: PromotionContext{
Expand Down
2 changes: 2 additions & 0 deletions ui/src/features/stage/create-stage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export const CreateStage = ({
promotionWizardStepsState.state?.map((step) => ({
uses: step?.identifier,
as: step?.as || '',
if: '',
config: step?.state as JSON, // step.state is type 'object' and it is safe to fake JSON type because it doesn't matter for stageFormToYAML function
vars: []
}))
Expand Down Expand Up @@ -178,6 +179,7 @@ export const CreateStage = ({
promotionWizardStepsState.state?.map((step) => ({
uses: step?.identifier,
as: step?.as || '',
if: '',
config: step?.state as JSON, // step.state is type 'object' and it is safe to fake JSON type because it doesn't matter for stageFormToYAML function
vars: []
}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
"description": "Config is opaque configuration for the PromotionStep that is understood\nonly by each PromotionStep's implementation. It is legal to utilize\nexpressions in defining values at any level of this block.\nSee https://docs.kargo.io/references/expression-language for details.",
"x-kubernetes-preserve-unknown-fields": true
},
"if": {
"description": "If is an optional expression that, if present, must evaluate to a boolean\nvalue. If the expression evaluates to false, the step will be skipped.",
"type": "string"
},
"retry": {
"description": "Retry is the retry policy for this step.",
"properties": {
Expand Down
4 changes: 4 additions & 0 deletions ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
"description": "Config is opaque configuration for the PromotionStep that is understood\nonly by each PromotionStep's implementation. It is legal to utilize\nexpressions in defining values at any level of this block.\nSee https://docs.kargo.io/references/expression-language for details.",
"x-kubernetes-preserve-unknown-fields": true
},
"if": {
"description": "If is an optional expression that, if present, must evaluate to a boolean\nvalue. If the expression evaluates to false, the step will be skipped.",
"type": "string"
},
"retry": {
"description": "Retry is the retry policy for this step.",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
"description": "Config is opaque configuration for the PromotionStep that is understood\nonly by each PromotionStep's implementation. It is legal to utilize\nexpressions in defining values at any level of this block.\nSee https://docs.kargo.io/references/expression-language for details.",
"x-kubernetes-preserve-unknown-fields": true
},
"if": {
"description": "If is an optional expression that, if present, must evaluate to a boolean\nvalue. If the expression evaluates to false, the step will be skipped.",
"type": "string"
},
"retry": {
"description": "Retry is the retry policy for this step.",
"properties": {
Expand Down
4 changes: 4 additions & 0 deletions ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
"description": "Config is opaque configuration for the PromotionStep that is understood\nonly by each PromotionStep's implementation. It is legal to utilize\nexpressions in defining values at any level of this block.\nSee https://docs.kargo.io/references/expression-language for details.",
"x-kubernetes-preserve-unknown-fields": true
},
"if": {
"description": "If is an optional expression that, if present, must evaluate to a boolean\nvalue. If the expression evaluates to false, the step will be skipped.",
"type": "string"
},
"retry": {
"description": "Retry is the retry policy for this step.",
"properties": {
Expand Down
10 changes: 9 additions & 1 deletion ui/src/gen/v1alpha1/generated_pb.ts

Large diffs are not rendered by default.

Loading