feat(ansible): expand templated loop sources
This commit is contained in:
parent
8012570663
commit
e1e2b6402e
3 changed files with 105 additions and 12 deletions
79
executor.go
79
executor.go
|
|
@ -8,6 +8,7 @@ import (
|
|||
"io/fs"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
|
@ -867,7 +868,7 @@ func (e *Executor) runLoop(ctx context.Context, host string, client sshExecutorC
|
|||
} else if task.WithTogether != nil {
|
||||
items, err = e.resolveWithTogetherLoop(task.WithTogether, host, task)
|
||||
} else {
|
||||
items = e.resolveLoop(task.Loop, host)
|
||||
items = e.resolveLoopWithTask(task.Loop, host, task)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -2461,6 +2462,10 @@ func (e *Executor) handleLookup(expr string) string {
|
|||
|
||||
// resolveLoop resolves loop items.
|
||||
func (e *Executor) resolveLoop(loop any, host string) []any {
|
||||
return e.resolveLoopWithTask(loop, host, nil)
|
||||
}
|
||||
|
||||
func (e *Executor) resolveLoopWithTask(loop any, host string, task *Task) []any {
|
||||
switch v := loop.(type) {
|
||||
case []any:
|
||||
return v
|
||||
|
|
@ -2471,10 +2476,17 @@ func (e *Executor) resolveLoop(loop any, host string) []any {
|
|||
}
|
||||
return items
|
||||
case string:
|
||||
if items, ok := e.resolveLoopExpression(v, host, task); ok {
|
||||
return items
|
||||
}
|
||||
|
||||
// Template the string and see if it's a var reference
|
||||
resolved := e.templateString(v, host, nil)
|
||||
if val, ok := e.vars[resolved]; ok {
|
||||
if items, ok := val.([]any); ok {
|
||||
resolved := e.templateString(v, host, task)
|
||||
if items, ok := anySliceFromValue(e.vars[resolved]); ok {
|
||||
return items
|
||||
}
|
||||
if task != nil {
|
||||
if items, ok := anySliceFromValue(task.Vars[resolved]); ok {
|
||||
return items
|
||||
}
|
||||
}
|
||||
|
|
@ -2482,6 +2494,65 @@ func (e *Executor) resolveLoop(loop any, host string) []any {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) resolveLoopExpression(loop string, host string, task *Task) ([]any, bool) {
|
||||
expr, ok := extractSingleTemplateExpression(loop)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if value, ok := e.lookupConditionValue(expr, host, task, nil); ok {
|
||||
if items, ok := anySliceFromValue(value); ok {
|
||||
return items, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func extractSingleTemplateExpression(value string) (string, bool) {
|
||||
re := regexp.MustCompile(`^\s*\{\{\s*(.+?)\s*\}\}\s*$`)
|
||||
match := re.FindStringSubmatch(value)
|
||||
if len(match) < 2 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
inner := strings.TrimSpace(match[1])
|
||||
if inner == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return inner, true
|
||||
}
|
||||
|
||||
func anySliceFromValue(value any) ([]any, bool) {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
return nil, false
|
||||
case []any:
|
||||
return append([]any(nil), v...), true
|
||||
case []string:
|
||||
items := make([]any, len(v))
|
||||
for i, item := range v {
|
||||
items[i] = item
|
||||
}
|
||||
return items, true
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(value)
|
||||
if !rv.IsValid() {
|
||||
return nil, false
|
||||
}
|
||||
if rv.Kind() != reflect.Slice && rv.Kind() != reflect.Array {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
items := make([]any, rv.Len())
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
items[i] = rv.Index(i).Interface()
|
||||
}
|
||||
return items, true
|
||||
}
|
||||
|
||||
// matchesTags checks if task tags match execution tags.
|
||||
func (e *Executor) matchesTags(taskTags []string) bool {
|
||||
// Tasks tagged "always" should run even when an explicit include filter is
|
||||
|
|
|
|||
|
|
@ -1574,6 +1574,31 @@ func TestExecutor_ResolveLoop_Good_Nil(t *testing.T) {
|
|||
assert.Nil(t, items)
|
||||
}
|
||||
|
||||
func TestExecutor_RunTaskOnHost_Good_LoopFromTemplatedListVariable(t *testing.T) {
|
||||
e := NewExecutor("/tmp")
|
||||
e.vars["items"] = []any{"alpha", "beta"}
|
||||
e.clients["host1"] = NewMockSSHClient()
|
||||
|
||||
task := &Task{
|
||||
Name: "Templated list loop",
|
||||
Module: "debug",
|
||||
Args: map[string]any{
|
||||
"msg": "{{ item }}",
|
||||
},
|
||||
Loop: "{{ items }}",
|
||||
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, "alpha", result.Results[0].Msg)
|
||||
assert.Equal(t, "beta", result.Results[1].Msg)
|
||||
}
|
||||
|
||||
// --- templateArgs ---
|
||||
|
||||
func TestExecutor_TemplateArgs_Good(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -581,15 +581,12 @@ func TestModulesInfra_ResolveLoop_Good_VarReference(t *testing.T) {
|
|||
e := NewExecutor("/tmp")
|
||||
e.vars["my_list"] = []any{"item1", "item2", "item3"}
|
||||
|
||||
// When loop is a string that resolves to a variable containing a list
|
||||
// A templated loop source should resolve to the underlying list value.
|
||||
items := e.resolveLoop("{{ my_list }}", "host1")
|
||||
// The template resolves "{{ my_list }}" but the result is a string representation,
|
||||
// not the original list. The resolveLoop handles this by trying to look up
|
||||
// the resolved value in vars again.
|
||||
// Since templateString returns the string "[item1 item2 item3]", and that
|
||||
// isn't a var name, items will be nil. This tests the edge case.
|
||||
// The actual var name lookup happens when the loop value is just "my_list".
|
||||
assert.Nil(t, items)
|
||||
require.Len(t, items, 3)
|
||||
assert.Equal(t, "item1", items[0])
|
||||
assert.Equal(t, "item2", items[1])
|
||||
assert.Equal(t, "item3", items[2])
|
||||
}
|
||||
|
||||
func TestModulesInfra_ResolveLoop_Good_MixedTypes(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue