feat(ansible): add with_subelements loop support
This commit is contained in:
parent
9f86b5cb95
commit
4e0a5f714c
5 changed files with 147 additions and 10 deletions
86
executor.go
86
executor.go
|
|
@ -676,8 +676,9 @@ func (e *Executor) runTaskOnHost(ctx context.Context, host string, hosts []strin
|
|||
return coreerr.E("Executor.runTaskOnHost", sprintf("get client for %s", executionHost), err)
|
||||
}
|
||||
|
||||
// Handle loops, including legacy with_file, with_fileglob, with_sequence, and with_together syntax.
|
||||
if task.Loop != nil || task.WithFile != nil || task.WithFileGlob != nil || task.WithSequence != nil || task.WithTogether != nil {
|
||||
// Handle loops, including legacy with_file, with_fileglob, with_sequence,
|
||||
// with_together, and with_subelements syntax.
|
||||
if task.Loop != nil || task.WithFile != nil || task.WithFileGlob != nil || task.WithSequence != nil || task.WithTogether != nil || task.WithSubelements != nil {
|
||||
return e.runLoop(ctx, host, client, task, play, start)
|
||||
}
|
||||
|
||||
|
|
@ -881,6 +882,8 @@ func (e *Executor) runLoop(ctx context.Context, host string, client sshExecutorC
|
|||
items, err = e.resolveWithSequenceLoop(task.WithSequence, host, task)
|
||||
} else if task.WithTogether != nil {
|
||||
items, err = e.resolveWithTogetherLoop(task.WithTogether, host, task)
|
||||
} else if task.WithSubelements != nil {
|
||||
items, err = e.resolveWithSubelementsLoop(task.WithSubelements, host, task)
|
||||
} else {
|
||||
items = e.resolveLoopWithTask(task.Loop, host, task)
|
||||
}
|
||||
|
|
@ -1167,6 +1170,85 @@ func (e *Executor) resolveWithTogetherLoop(loop any, host string, task *Task) ([
|
|||
return items, nil
|
||||
}
|
||||
|
||||
func (e *Executor) resolveWithSubelementsLoop(loop any, host string, task *Task) ([]any, error) {
|
||||
source, subelement, ok := parseSubelementsSpec(loop)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
if subelement == "" {
|
||||
return nil, coreerr.E("Executor.resolveWithSubelementsLoop", "with_subelements requires a subelement name", nil)
|
||||
}
|
||||
|
||||
parents := e.resolveSubelementsParents(source, host, task)
|
||||
items := make([]any, 0)
|
||||
for _, parent := range parents {
|
||||
for _, subitem := range subelementItems(parent, subelement) {
|
||||
items = append(items, []any{parent, subitem})
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func parseSubelementsSpec(loop any) (any, string, bool) {
|
||||
switch v := loop.(type) {
|
||||
case []any:
|
||||
if len(v) < 2 {
|
||||
return nil, "", false
|
||||
}
|
||||
return v[0], sprintf("%v", v[1]), true
|
||||
case []string:
|
||||
if len(v) < 2 {
|
||||
return nil, "", false
|
||||
}
|
||||
return v[0], v[1], true
|
||||
case string:
|
||||
parts := strings.Fields(v)
|
||||
if len(parts) < 2 {
|
||||
return nil, "", false
|
||||
}
|
||||
return parts[0], parts[1], true
|
||||
default:
|
||||
return nil, "", false
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) resolveSubelementsParents(value any, host string, task *Task) []any {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if items := e.resolveLoopWithTask(v, host, task); len(items) > 0 {
|
||||
return items
|
||||
}
|
||||
if items, ok := anySliceFromValue(e.templateString(v, host, task)); ok {
|
||||
return items
|
||||
}
|
||||
if task != nil {
|
||||
if items, ok := anySliceFromValue(task.Vars[v]); ok {
|
||||
return items
|
||||
}
|
||||
}
|
||||
default:
|
||||
if items, ok := anySliceFromValue(v); ok {
|
||||
return items
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func subelementItems(parent any, path string) []any {
|
||||
value, ok := lookupNestedValue(parent, path)
|
||||
if !ok || value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if items, ok := anySliceFromValue(value); ok {
|
||||
return items
|
||||
}
|
||||
|
||||
return []any{value}
|
||||
}
|
||||
|
||||
func parseSequenceSpec(loop any) (*sequenceSpec, error) {
|
||||
spec := &sequenceSpec{
|
||||
step: 1,
|
||||
|
|
|
|||
|
|
@ -1003,6 +1003,41 @@ func TestExecutor_RunTaskOnHost_Good_LoopFromWithTogether(t *testing.T) {
|
|||
assert.Equal(t, "blue=large", result.Results[1].Msg)
|
||||
}
|
||||
|
||||
func TestExecutor_RunTaskOnHost_Good_LoopFromWithSubelements(t *testing.T) {
|
||||
e := NewExecutor("/tmp")
|
||||
e.clients["host1"] = NewMockSSHClient()
|
||||
e.vars["users"] = []any{
|
||||
map[string]any{
|
||||
"name": "alice",
|
||||
"authorized": []any{"ssh-rsa AAA", "ssh-ed25519 BBB"},
|
||||
},
|
||||
map[string]any{
|
||||
"name": "bob",
|
||||
"authorized": "ssh-rsa CCC",
|
||||
},
|
||||
}
|
||||
|
||||
task := &Task{
|
||||
Name: "Subelements loop",
|
||||
Module: "debug",
|
||||
Args: map[string]any{
|
||||
"msg": "{{ item.0.name }}={{ item.1 }}",
|
||||
},
|
||||
WithSubelements: []any{"{{ users }}", "authorized"},
|
||||
Register: "subelements_loop_result",
|
||||
}
|
||||
|
||||
err := e.runTaskOnHosts(context.Background(), []string{"host1"}, task, &Play{})
|
||||
require.NoError(t, err)
|
||||
|
||||
result := e.results["host1"]["subelements_loop_result"]
|
||||
require.NotNil(t, result)
|
||||
require.Len(t, result.Results, 3)
|
||||
assert.Equal(t, "alice=ssh-rsa AAA", result.Results[0].Msg)
|
||||
assert.Equal(t, "alice=ssh-ed25519 BBB", result.Results[1].Msg)
|
||||
assert.Equal(t, "bob=ssh-rsa CCC", result.Results[2].Msg)
|
||||
}
|
||||
|
||||
func TestExecutor_RunTaskOnHosts_Good_LoopNotifiesAndCallsCallback(t *testing.T) {
|
||||
e := NewExecutor("/tmp")
|
||||
e.clients["host1"] = NewMockSSHClient()
|
||||
|
|
|
|||
|
|
@ -357,7 +357,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
|||
"action": true, "local_action": true,
|
||||
"include_tasks": true, "import_tasks": true,
|
||||
"include_role": true, "import_role": true,
|
||||
"with_items": true, "with_dict": true, "with_indexed_items": true, "with_nested": true, "with_together": true, "with_file": true, "with_fileglob": true, "with_sequence": true,
|
||||
"with_items": true, "with_dict": true, "with_indexed_items": true, "with_nested": true, "with_together": true, "with_subelements": true, "with_file": true, "with_fileglob": true, "with_sequence": true,
|
||||
}
|
||||
|
||||
for key, val := range m {
|
||||
|
|
|
|||
15
types.go
15
types.go
|
|
@ -117,13 +117,14 @@ type Task struct {
|
|||
Until string `yaml:"until,omitempty"`
|
||||
|
||||
// Include/import directives
|
||||
IncludeTasks string `yaml:"include_tasks,omitempty"`
|
||||
ImportTasks string `yaml:"import_tasks,omitempty"`
|
||||
WithFile any `yaml:"with_file,omitempty"`
|
||||
WithFileGlob any `yaml:"with_fileglob,omitempty"`
|
||||
WithSequence any `yaml:"with_sequence,omitempty"`
|
||||
WithTogether any `yaml:"with_together,omitempty"`
|
||||
IncludeRole *struct {
|
||||
IncludeTasks string `yaml:"include_tasks,omitempty"`
|
||||
ImportTasks string `yaml:"import_tasks,omitempty"`
|
||||
WithFile any `yaml:"with_file,omitempty"`
|
||||
WithFileGlob any `yaml:"with_fileglob,omitempty"`
|
||||
WithSequence any `yaml:"with_sequence,omitempty"`
|
||||
WithTogether any `yaml:"with_together,omitempty"`
|
||||
WithSubelements any `yaml:"with_subelements,omitempty"`
|
||||
IncludeRole *struct {
|
||||
Name string `yaml:"name"`
|
||||
TasksFrom string `yaml:"tasks_from,omitempty"`
|
||||
DefaultsFrom string `yaml:"defaults_from,omitempty"`
|
||||
|
|
|
|||
|
|
@ -400,6 +400,25 @@ with_together:
|
|||
assert.Equal(t, []any{"blue", "large"}, second)
|
||||
}
|
||||
|
||||
func TestTypes_Task_UnmarshalYAML_Good_WithSubelements(t *testing.T) {
|
||||
input := `
|
||||
name: Subelement loop values
|
||||
debug:
|
||||
msg: "{{ item.0.name }} {{ item.1 }}"
|
||||
with_subelements:
|
||||
- users
|
||||
- authorized
|
||||
`
|
||||
var task Task
|
||||
err := yaml.Unmarshal([]byte(input), &task)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, task.WithSubelements)
|
||||
values, ok := task.WithSubelements.([]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []any{"users", "authorized"}, values)
|
||||
}
|
||||
|
||||
func TestTypes_Task_UnmarshalYAML_Good_WithNotify(t *testing.T) {
|
||||
input := `
|
||||
name: Install package
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue