Skip to content

Commit f92bcf7

Browse files
committed
amp: when conditional
1 parent 83388d1 commit f92bcf7

14 files changed

Lines changed: 712 additions & 77 deletions

File tree

AGENTS.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3770,6 +3770,47 @@ tasks:
37703770
- If inventory file is not found, dibra errors
37713771
- Circular group references are detected and reported
37723772

3773+
## when
3774+
3775+
Conditionally executes a task. The condition is evaluated against the same variable context used for templates (`vars`, `hostvars`, `inventory_hostname`, `group_names`, registered results, etc.). If the condition resolves to false, the task is skipped. If the condition cannot be evaluated, the task fails.
3776+
3777+
**Syntax**:
3778+
- String expression using Jinja-style syntax (`==`, `!=`, `>`, `>=`, `<`, `<=`, `and`, `or`, `not`, `in`, `is defined`, filters like `default`, `length`)
3779+
- Boolean literal (`true`/`false`)
3780+
- Number (`0` is false, non-zero is true)
3781+
- List of conditions (all must be true)
3782+
3783+
```yaml
3784+
- name: Run only on web hosts
3785+
copy:
3786+
content: "web"
3787+
dest: /tmp/web.txt
3788+
when: '"web" in group_names'
3789+
3790+
- name: Multiple conditions (AND)
3791+
copy:
3792+
content: "ready"
3793+
dest: /tmp/ready.txt
3794+
when:
3795+
- app.enabled
3796+
- (app.port | int) > 1024
3797+
3798+
- name: Use filters
3799+
copy:
3800+
content: "fallback"
3801+
dest: /tmp/fallback.txt
3802+
when: (missing_value | default("fallback")) == "fallback"
3803+
3804+
- name: Boolean literal
3805+
ping:
3806+
when: true
3807+
```
3808+
3809+
**Behavior**:
3810+
- Skipped tasks report `skipped: true`, `changed: false`, and `msg: "when condition false"` in register results.
3811+
- `include_tasks`: if the `when` condition is false, no tasks are included.
3812+
- `import_tasks`: parent `when` conditions are merged with imported task conditions (logical AND).
3813+
37733814
## import_tasks
37743815

37753816
Imports a list of tasks from another YAML file, inserting them into the current playbook at parse time (static include). This is a controller-side directive, not a remote module.

cmd/controller/main.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type ModuleRequest struct {
4747
type GenericResponse struct {
4848
Changed bool `json:"changed"`
4949
Failed bool `json:"failed"`
50+
Skipped bool `json:"skipped"`
5051
Msg string `json:"msg,omitempty"`
5152
RC int `json:"rc"`
5253
Stdout string `json:"stdout,omitempty"`
@@ -461,6 +462,31 @@ func main() {
461462
hostContext["vars"] = resolved.Namespaces
462463
flattened := hostContext
463464

465+
if len(task.When) > 0 {
466+
shouldRun, err := template.EvaluateWhen([]interface{}(task.When), flattened)
467+
if err != nil {
468+
fmt.Printf(" ✗ FAILED: when condition error: %v\n", err)
469+
if task.Register != "" {
470+
registerResult(hostRuntimeVars, host.Name, task, map[string]interface{}{
471+
"failed": true,
472+
"msg": fmt.Sprintf("when condition error: %v", err),
473+
})
474+
}
475+
continue
476+
}
477+
if !shouldRun {
478+
fmt.Printf(" ↷ SKIPPED (when condition false)\n")
479+
if task.Register != "" {
480+
registerResult(hostRuntimeVars, host.Name, task, map[string]interface{}{
481+
"changed": false,
482+
"skipped": true,
483+
"msg": "when condition false",
484+
})
485+
}
486+
continue
487+
}
488+
}
489+
464490
var modReq ModuleRequest
465491

466492
switch {
@@ -2383,7 +2409,13 @@ func main() {
23832409
continue
23842410
}
23852411

2386-
if resp.Failed {
2412+
if resp.Skipped {
2413+
fmt.Printf(" ↷ SKIPPED")
2414+
if resp.Msg != "" {
2415+
fmt.Printf(" - %s", resp.Msg)
2416+
}
2417+
fmt.Println()
2418+
} else if resp.Failed {
23872419
fmt.Printf(" ✗ FAILED: %s\n", resp.Msg)
23882420
if *verbose && resp.Stderr != "" {
23892421
fmt.Printf(" Stderr: %s\n", resp.Stderr)
@@ -2496,6 +2528,7 @@ func genericResponseToMap(resp GenericResponse) map[string]interface{} {
24962528
result := map[string]interface{}{
24972529
"changed": resp.Changed,
24982530
"failed": resp.Failed,
2531+
"skipped": resp.Skipped,
24992532
"rc": resp.RC,
25002533
"msg": resp.Msg,
25012534
"stdout": resp.Stdout,

cue/schema/base.cue

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ package schema
1919
}
2020

2121
#Task: {
22-
name: string
23-
register?: string
24-
vars?: {[string]: _}
25-
import_tasks?: string | #ImportTasks
26-
include_tasks?: string | #IncludeTasks
22+
name: string
23+
register?: string
24+
vars?: {[string]: _}
25+
when?: string | bool | number | [...(string|bool|number)]
26+
import_tasks?: string | #ImportTasks
27+
include_tasks?: string | #IncludeTasks
2728

2829
{ping: #Ping} |
2930
{apt: #Apt} |

internal/config/config.go

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package config
22

33
import (
44
"bytes"
5+
"encoding/json"
56
"fmt"
67
"os"
78
"reflect"
@@ -67,9 +68,70 @@ func (p *IncludeTasksParams) UnmarshalYAML(node *yaml.Node) error {
6768
return nil
6869
}
6970

71+
type When []interface{}
72+
73+
func (w *When) UnmarshalYAML(node *yaml.Node) error {
74+
var raw interface{}
75+
if err := node.Decode(&raw); err != nil {
76+
return err
77+
}
78+
normalized, err := normalizeWhenValue(raw)
79+
if err != nil {
80+
return err
81+
}
82+
*w = normalized
83+
return nil
84+
}
85+
86+
func (w *When) UnmarshalJSON(data []byte) error {
87+
var raw interface{}
88+
if err := json.Unmarshal(data, &raw); err != nil {
89+
return err
90+
}
91+
normalized, err := normalizeWhenValue(raw)
92+
if err != nil {
93+
return err
94+
}
95+
*w = normalized
96+
return nil
97+
}
98+
99+
func normalizeWhenValue(raw interface{}) (When, error) {
100+
if raw == nil {
101+
return nil, nil
102+
}
103+
104+
switch v := raw.(type) {
105+
case When:
106+
return v, nil
107+
case string, bool, int, int32, int64, float32, float64, uint, uint32, uint64:
108+
return When{v}, nil
109+
case []string:
110+
normalized := make(When, len(v))
111+
for i, item := range v {
112+
normalized[i] = item
113+
}
114+
return normalized, nil
115+
case []interface{}:
116+
normalized := make(When, len(v))
117+
for i, item := range v {
118+
switch item.(type) {
119+
case string, bool, int, int32, int64, float32, float64, uint, uint32, uint64:
120+
normalized[i] = item
121+
default:
122+
return nil, fmt.Errorf("when list entries must be strings, booleans, or numbers")
123+
}
124+
}
125+
return normalized, nil
126+
default:
127+
return nil, fmt.Errorf("when must be a string, boolean, number, or list")
128+
}
129+
}
130+
70131
type Task struct {
71132
Name string `json:"name" yaml:"name"`
72133
Vars map[string]interface{} `json:"vars,omitempty" yaml:"vars,omitempty"`
134+
When When `json:"when,omitempty" yaml:"when,omitempty"`
73135
ImportTasks *ImportTasksParams `json:"import_tasks,omitempty" yaml:"import_tasks,omitempty"`
74136
IncludeTasks *IncludeTasksParams `json:"include_tasks,omitempty" yaml:"include_tasks,omitempty"`
75137
SourceDir string `yaml:"-"`
@@ -135,24 +197,24 @@ type Task struct {
135197
}
136198

137199
type TemplateParams struct {
138-
Src string `json:"src" yaml:"src"`
139-
Dest string `json:"dest" yaml:"dest"`
140-
Mode string `json:"mode,omitempty" yaml:"mode,omitempty"`
141-
Owner string `json:"owner,omitempty" yaml:"owner,omitempty"`
142-
Group string `json:"group,omitempty" yaml:"group,omitempty"`
143-
Backup bool `json:"backup" yaml:"backup"`
144-
Force *bool `json:"force,omitempty" yaml:"force,omitempty"`
145-
Follow bool `json:"follow" yaml:"follow"`
146-
Validate string `json:"validate,omitempty" yaml:"validate,omitempty"`
147-
NewlineSequence string `json:"newline_sequence,omitempty" yaml:"newline_sequence,omitempty"`
200+
Src string `json:"src" yaml:"src"`
201+
Dest string `json:"dest" yaml:"dest"`
202+
Mode string `json:"mode,omitempty" yaml:"mode,omitempty"`
203+
Owner string `json:"owner,omitempty" yaml:"owner,omitempty"`
204+
Group string `json:"group,omitempty" yaml:"group,omitempty"`
205+
Backup bool `json:"backup" yaml:"backup"`
206+
Force *bool `json:"force,omitempty" yaml:"force,omitempty"`
207+
Follow bool `json:"follow" yaml:"follow"`
208+
Validate string `json:"validate,omitempty" yaml:"validate,omitempty"`
209+
NewlineSequence string `json:"newline_sequence,omitempty" yaml:"newline_sequence,omitempty"`
148210
VariableStartString string `json:"variable_start_string,omitempty" yaml:"variable_start_string,omitempty"`
149211
VariableEndString string `json:"variable_end_string,omitempty" yaml:"variable_end_string,omitempty"`
150212
BlockStartString string `json:"block_start_string,omitempty" yaml:"block_start_string,omitempty"`
151213
BlockEndString string `json:"block_end_string,omitempty" yaml:"block_end_string,omitempty"`
152214
CommentStartString string `json:"comment_start_string,omitempty" yaml:"comment_start_string,omitempty"`
153215
CommentEndString string `json:"comment_end_string,omitempty" yaml:"comment_end_string,omitempty"`
154-
TrimBlocks *bool `json:"trim_blocks,omitempty" yaml:"trim_blocks,omitempty"`
155-
LstripBlocks *bool `json:"lstrip_blocks,omitempty" yaml:"lstrip_blocks,omitempty"`
216+
TrimBlocks *bool `json:"trim_blocks,omitempty" yaml:"trim_blocks,omitempty"`
217+
LstripBlocks *bool `json:"lstrip_blocks,omitempty" yaml:"lstrip_blocks,omitempty"`
156218
}
157219

158220
type DockerSwarmServiceParams struct {
@@ -820,9 +882,9 @@ type DockerImageParams struct {
820882
Pull interface{} `json:"pull,omitempty" yaml:"pull,omitempty"` // string (missing/always/never) or bool for backward compat
821883

822884
// Force flags (separated for clarity)
823-
ForcePull bool `json:"force_pull,omitempty" yaml:"force_pull,omitempty"` // Force pull even if image exists
885+
ForcePull bool `json:"force_pull,omitempty" yaml:"force_pull,omitempty"` // Force pull even if image exists
824886
ForceRemove bool `json:"force_remove,omitempty" yaml:"force_remove,omitempty"` // Force remove (removes containers using the image)
825-
ForceTag bool `json:"force_tag,omitempty" yaml:"force_tag,omitempty"` // Force tag even if target exists
887+
ForceTag bool `json:"force_tag,omitempty" yaml:"force_tag,omitempty"` // Force tag even if target exists
826888
ForceSource bool `json:"force_source,omitempty" yaml:"force_source,omitempty"` // Deprecated: use force_pull or force_remove
827889

828890
// Registry authentication

internal/config/import_tasks.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ func expandImportTasks(tasks []Task, baseDir string, renderPath func(string) (st
7777
}
7878
}
7979

80+
if len(task.When) > 0 {
81+
for i := range importedTasks {
82+
importedTasks[i].When = mergeWhen(task.When, importedTasks[i].When)
83+
}
84+
}
85+
8086
newStack := append(append([]string{}, stack...), absPath)
8187
newBaseDir := filepath.Dir(absPath)
8288
expanded, err := expandImportTasks(importedTasks, newBaseDir, renderPath, newStack, depth+1)
@@ -90,6 +96,19 @@ func expandImportTasks(tasks []Task, baseDir string, renderPath func(string) (st
9096
return result, nil
9197
}
9298

99+
func mergeWhen(parent When, child When) When {
100+
if len(parent) == 0 {
101+
return child
102+
}
103+
if len(child) == 0 {
104+
return parent
105+
}
106+
merged := make(When, 0, len(parent)+len(child))
107+
merged = append(merged, parent...)
108+
merged = append(merged, child...)
109+
return merged
110+
}
111+
93112
func loadTasksFile(path string) ([]Task, error) {
94113
data, err := os.ReadFile(path)
95114
if err != nil {

0 commit comments

Comments
 (0)