feat(ansible): expose extended loop metadata
This commit is contained in:
parent
6cc987ea74
commit
716ad80951
2 changed files with 107 additions and 0 deletions
79
executor.go
79
executor.go
|
|
@ -635,6 +635,12 @@ func (e *Executor) runLoop(ctx context.Context, host string, client sshExecutorC
|
|||
savedVars[indexVar] = v
|
||||
}
|
||||
}
|
||||
var savedLoopMeta any
|
||||
if task.LoopControl != nil && task.LoopControl.Extended {
|
||||
if v, ok := e.vars["ansible_loop"]; ok {
|
||||
savedLoopMeta = v
|
||||
}
|
||||
}
|
||||
|
||||
var results []TaskResult
|
||||
for i, item := range items {
|
||||
|
|
@ -643,6 +649,21 @@ func (e *Executor) runLoop(ctx context.Context, host string, client sshExecutorC
|
|||
if indexVar != "" {
|
||||
e.vars[indexVar] = i
|
||||
}
|
||||
if task.LoopControl != nil && task.LoopControl.Extended {
|
||||
loopMeta := map[string]any{
|
||||
"index": i + 1,
|
||||
"index0": i,
|
||||
"first": i == 0,
|
||||
"last": i == len(items)-1,
|
||||
"length": len(items),
|
||||
"revindex": len(items) - i,
|
||||
"revindex0": len(items) - i - 1,
|
||||
}
|
||||
if task.LoopControl.Label != "" {
|
||||
loopMeta["label"] = e.templateString(task.LoopControl.Label, host, task)
|
||||
}
|
||||
e.vars["ansible_loop"] = loopMeta
|
||||
}
|
||||
|
||||
result, err := e.runTaskWithRetries(ctx, host, task, play, func() (*TaskResult, error) {
|
||||
return e.executeModule(ctx, host, client, task, play)
|
||||
|
|
@ -680,6 +701,13 @@ func (e *Executor) runLoop(ctx context.Context, host string, client sshExecutorC
|
|||
delete(e.vars, indexVar)
|
||||
}
|
||||
}
|
||||
if task.LoopControl != nil && task.LoopControl.Extended {
|
||||
if savedLoopMeta != nil {
|
||||
e.vars["ansible_loop"] = savedLoopMeta
|
||||
} else {
|
||||
delete(e.vars, "ansible_loop")
|
||||
}
|
||||
}
|
||||
|
||||
// Store combined result
|
||||
if task.Register != "" {
|
||||
|
|
@ -1338,6 +1366,16 @@ func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
|
|||
}
|
||||
}
|
||||
|
||||
// Resolve nested maps from vars, task vars, or host vars.
|
||||
if contains(expr, ".") {
|
||||
parts := splitN(expr, ".", 2)
|
||||
if val, ok := e.lookupExprValue(parts[0], host, task); ok {
|
||||
if nested, ok := lookupNestedValue(val, parts[1]); ok {
|
||||
return sprintf("%v", nested)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check vars
|
||||
if val, ok := e.vars[expr]; ok {
|
||||
return sprintf("%v", val)
|
||||
|
|
@ -1387,6 +1425,47 @@ func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
|
|||
return "{{ " + expr + " }}" // Return as-is if unresolved
|
||||
}
|
||||
|
||||
// lookupExprValue resolves the first segment of an expression against the
|
||||
// executor, task, and inventory scopes.
|
||||
func (e *Executor) lookupExprValue(name string, host string, task *Task) (any, bool) {
|
||||
if val, ok := e.vars[name]; ok {
|
||||
return val, true
|
||||
}
|
||||
if task != nil {
|
||||
if val, ok := task.Vars[name]; ok {
|
||||
return val, true
|
||||
}
|
||||
}
|
||||
if e.inventory != nil {
|
||||
hostVars := GetHostVars(e.inventory, host)
|
||||
if val, ok := hostVars[name]; ok {
|
||||
return val, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// lookupNestedValue walks a dotted path through nested maps.
|
||||
func lookupNestedValue(value any, path string) (any, bool) {
|
||||
if path == "" {
|
||||
return value, true
|
||||
}
|
||||
|
||||
current := value
|
||||
for _, segment := range split(path, ".") {
|
||||
next, ok := current.(map[string]any)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
current, ok = next[segment]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
return current, true
|
||||
}
|
||||
|
||||
// applyFilter applies a Jinja2 filter.
|
||||
func (e *Executor) applyFilter(value, filter string) string {
|
||||
filter = corexTrimSpace(filter)
|
||||
|
|
|
|||
|
|
@ -291,6 +291,34 @@ func TestExecutor_RunTaskOnHost_Good_LoopControlPause(t *testing.T) {
|
|||
assert.GreaterOrEqual(t, elapsed, 900*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestExecutor_RunTaskOnHost_Good_LoopControlExtendedExposesMetadata(t *testing.T) {
|
||||
e := NewExecutor("/tmp")
|
||||
e.clients["host1"] = NewMockSSHClient()
|
||||
|
||||
task := &Task{
|
||||
Name: "Extended loop metadata",
|
||||
Module: "debug",
|
||||
Args: map[string]any{
|
||||
"msg": "{{ ansible_loop.label }} {{ ansible_loop.index0 }}/{{ ansible_loop.length }} first={{ ansible_loop.first }} last={{ ansible_loop.last }}",
|
||||
},
|
||||
Loop: []any{"one", "two"},
|
||||
LoopControl: &LoopControl{
|
||||
Extended: true,
|
||||
Label: "{{ item }}",
|
||||
},
|
||||
Register: "loop_result",
|
||||
}
|
||||
|
||||
err := e.runTaskOnHosts(context.Background(), []string{"host1"}, task, &Play{})
|
||||
require.NoError(t, err)
|
||||
|
||||
result := e.results["host1"]["loop_result"]
|
||||
require.NotNil(t, result)
|
||||
require.Len(t, result.Results, 2)
|
||||
assert.Equal(t, "one 0/2 first=true last=false", result.Results[0].Msg)
|
||||
assert.Equal(t, "two 1/2 first=false last=true", result.Results[1].Msg)
|
||||
}
|
||||
|
||||
func TestExecutor_RunTaskWithRetries_Good_UntilSuccess(t *testing.T) {
|
||||
e := NewExecutor("/tmp")
|
||||
attempts := 0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue