[agent/codex:gpt-5.4-mini] Read ~/spec/code/core/go/ansible/RFC.md fully. Find ONE feat... #29
2 changed files with 142 additions and 35 deletions
147
executor.go
147
executor.go
|
|
@ -623,6 +623,10 @@ func (e *Executor) runLoop(ctx context.Context, host string, client *SSHClient,
|
|||
savedVars[indexVar] = v
|
||||
}
|
||||
}
|
||||
extendedLoop := task.LoopControl != nil && task.LoopControl.Extended
|
||||
if v, ok := e.vars["ansible_loop"]; ok {
|
||||
savedVars["ansible_loop"] = v
|
||||
}
|
||||
|
||||
var results []TaskResult
|
||||
for i, item := range items {
|
||||
|
|
@ -631,6 +635,11 @@ func (e *Executor) runLoop(ctx context.Context, host string, client *SSHClient,
|
|||
if indexVar != "" {
|
||||
e.vars[indexVar] = i
|
||||
}
|
||||
if extendedLoop {
|
||||
e.vars["ansible_loop"] = e.loopMetadata(item, i, len(items), task.LoopControl.Label, host, task)
|
||||
} else {
|
||||
delete(e.vars, "ansible_loop")
|
||||
}
|
||||
|
||||
result, err := e.executeModule(ctx, host, client, task, play)
|
||||
if err != nil {
|
||||
|
|
@ -660,6 +669,11 @@ func (e *Executor) runLoop(ctx context.Context, host string, client *SSHClient,
|
|||
delete(e.vars, indexVar)
|
||||
}
|
||||
}
|
||||
if v, ok := savedVars["ansible_loop"]; ok {
|
||||
e.vars["ansible_loop"] = v
|
||||
} else {
|
||||
delete(e.vars, "ansible_loop")
|
||||
}
|
||||
|
||||
// Store combined result
|
||||
if task.Register != "" {
|
||||
|
|
@ -1048,6 +1062,96 @@ func (e *Executor) getRegisteredVar(host string, name string) *TaskResult {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) lookupTemplateValue(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
|
||||
}
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "ansible_hostname":
|
||||
if facts, ok := e.facts[host]; ok {
|
||||
return facts.Hostname, true
|
||||
}
|
||||
case "ansible_fqdn":
|
||||
if facts, ok := e.facts[host]; ok {
|
||||
return facts.FQDN, true
|
||||
}
|
||||
case "ansible_distribution":
|
||||
if facts, ok := e.facts[host]; ok {
|
||||
return facts.Distribution, true
|
||||
}
|
||||
case "ansible_distribution_version":
|
||||
if facts, ok := e.facts[host]; ok {
|
||||
return facts.Version, true
|
||||
}
|
||||
case "ansible_architecture":
|
||||
if facts, ok := e.facts[host]; ok {
|
||||
return facts.Architecture, true
|
||||
}
|
||||
case "ansible_kernel":
|
||||
if facts, ok := e.facts[host]; ok {
|
||||
return facts.Kernel, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func resolveDottedValue(value any, path string) (any, bool) {
|
||||
current := value
|
||||
for _, part := range split(path, ".") {
|
||||
switch v := current.(type) {
|
||||
case map[string]any:
|
||||
next, ok := v[part]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
current = next
|
||||
case map[string]string:
|
||||
next, ok := v[part]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
current = next
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return current, true
|
||||
}
|
||||
|
||||
func (e *Executor) loopMetadata(item any, index, length int, label string, host string, task *Task) map[string]any {
|
||||
metadata := map[string]any{
|
||||
"index": index + 1,
|
||||
"index0": index,
|
||||
"revindex": length - index,
|
||||
"revindex0": length - index - 1,
|
||||
"first": index == 0,
|
||||
"last": index == length-1,
|
||||
"length": length,
|
||||
"item": item,
|
||||
}
|
||||
|
||||
if label != "" {
|
||||
metadata["label"] = e.templateString(label, host, task)
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
// templateString applies Jinja2-like templating.
|
||||
func (e *Executor) templateString(s string, host string, task *Task) string {
|
||||
// Handle {{ var }} syntax
|
||||
|
|
@ -1090,46 +1194,19 @@ func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
|
|||
return sprintf("%t", result.Failed)
|
||||
}
|
||||
}
|
||||
|
||||
if root, ok := e.lookupTemplateValue(parts[0], host, task); ok {
|
||||
if resolved, ok := resolveDottedValue(root, parts[1]); ok {
|
||||
return sprintf("%v", resolved)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check vars
|
||||
if val, ok := e.vars[expr]; ok {
|
||||
// Check vars, task vars, and host vars
|
||||
if val, ok := e.lookupTemplateValue(expr, host, task); ok {
|
||||
return sprintf("%v", val)
|
||||
}
|
||||
|
||||
// Check task vars
|
||||
if task != nil {
|
||||
if val, ok := task.Vars[expr]; ok {
|
||||
return sprintf("%v", val)
|
||||
}
|
||||
}
|
||||
|
||||
// Check host vars
|
||||
if e.inventory != nil {
|
||||
hostVars := GetHostVars(e.inventory, host)
|
||||
if val, ok := hostVars[expr]; ok {
|
||||
return sprintf("%v", val)
|
||||
}
|
||||
}
|
||||
|
||||
// Check facts
|
||||
if facts, ok := e.facts[host]; ok {
|
||||
switch expr {
|
||||
case "ansible_hostname":
|
||||
return facts.Hostname
|
||||
case "ansible_fqdn":
|
||||
return facts.FQDN
|
||||
case "ansible_distribution":
|
||||
return facts.Distribution
|
||||
case "ansible_distribution_version":
|
||||
return facts.Version
|
||||
case "ansible_architecture":
|
||||
return facts.Architecture
|
||||
case "ansible_kernel":
|
||||
return facts.Kernel
|
||||
}
|
||||
}
|
||||
|
||||
return "{{ " + expr + " }}" // Return as-is if unresolved
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -459,3 +459,33 @@ func TestExecutorExtra_ResolveExpr_Good_WithFilter(t *testing.T) {
|
|||
result := e.resolveExpr("raw_value | trim", "host1", nil)
|
||||
assert.Equal(t, "trimmed", result)
|
||||
}
|
||||
|
||||
func TestExecutorExtra_LoopMetadata_Good_ExtendedMetadata(t *testing.T) {
|
||||
e := NewExecutor("/tmp")
|
||||
e.vars["item"] = "alpha"
|
||||
|
||||
metadata := e.loopMetadata("alpha", 1, 3, "item {{ item }}", "host1", &Task{})
|
||||
|
||||
assert.Equal(t, 2, metadata["index"])
|
||||
assert.Equal(t, 1, metadata["index0"])
|
||||
assert.Equal(t, 2, metadata["revindex"])
|
||||
assert.Equal(t, 1, metadata["revindex0"])
|
||||
assert.False(t, metadata["first"].(bool))
|
||||
assert.False(t, metadata["last"].(bool))
|
||||
assert.Equal(t, 3, metadata["length"])
|
||||
assert.Equal(t, "alpha", metadata["item"])
|
||||
assert.Equal(t, "item alpha", metadata["label"])
|
||||
}
|
||||
|
||||
func TestExecutorExtra_TemplateString_Good_DottedMapAccess(t *testing.T) {
|
||||
e := NewExecutor("/tmp")
|
||||
e.vars["ansible_loop"] = map[string]any{
|
||||
"index0": 1,
|
||||
"first": false,
|
||||
"last": true,
|
||||
}
|
||||
|
||||
got := e.templateString("{{ ansible_loop.index0 }} {{ ansible_loop.first }} {{ ansible_loop.last }}", "host1", nil)
|
||||
|
||||
assert.Equal(t, "1 false true", got)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue