Add with_file loop support
This commit is contained in:
parent
df8a400553
commit
097aeec0d2
5 changed files with 128 additions and 3 deletions
72
executor.go
72
executor.go
|
|
@ -549,8 +549,8 @@ 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
|
||||
if task.Loop != nil {
|
||||
// Handle loops, including legacy with_file syntax.
|
||||
if task.Loop != nil || task.WithFile != nil {
|
||||
return e.runLoop(ctx, host, client, task, play)
|
||||
}
|
||||
|
||||
|
|
@ -704,7 +704,18 @@ func shouldRetryTask(task *Task, host string, e *Executor, result *TaskResult) b
|
|||
|
||||
// runLoop handles task loops.
|
||||
func (e *Executor) runLoop(ctx context.Context, host string, client sshExecutorClient, task *Task, play *Play) error {
|
||||
items := e.resolveLoop(task.Loop, host)
|
||||
var (
|
||||
items []any
|
||||
err error
|
||||
)
|
||||
if task.WithFile != nil {
|
||||
items, err = e.resolveWithFileLoop(task.WithFile, host, task)
|
||||
} else {
|
||||
items = e.resolveLoop(task.Loop, host)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loopVar := "item"
|
||||
if task.LoopControl != nil && task.LoopControl.LoopVar != "" {
|
||||
|
|
@ -817,6 +828,61 @@ func (e *Executor) runLoop(ctx context.Context, host string, client sshExecutorC
|
|||
return nil
|
||||
}
|
||||
|
||||
// resolveWithFileLoop resolves legacy with_file loop items into file contents.
|
||||
func (e *Executor) resolveWithFileLoop(loop any, host string, task *Task) ([]any, error) {
|
||||
var paths []string
|
||||
|
||||
switch v := loop.(type) {
|
||||
case []any:
|
||||
paths = make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
s := sprintf("%v", item)
|
||||
if s = e.templateString(s, host, task); s != "" {
|
||||
paths = append(paths, s)
|
||||
}
|
||||
}
|
||||
case []string:
|
||||
paths = make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if s := e.templateString(item, host, task); s != "" {
|
||||
paths = append(paths, s)
|
||||
}
|
||||
}
|
||||
case string:
|
||||
if s := e.templateString(v, host, task); s != "" {
|
||||
paths = []string{s}
|
||||
}
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
items := make([]any, 0, len(paths))
|
||||
for _, filePath := range paths {
|
||||
content, err := e.readLoopFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, content)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (e *Executor) readLoopFile(filePath string) (string, error) {
|
||||
candidates := []string{filePath}
|
||||
if e.parser != nil && e.parser.basePath != "" {
|
||||
candidates = append([]string{joinPath(e.parser.basePath, filePath)}, candidates...)
|
||||
}
|
||||
|
||||
for _, candidate := range candidates {
|
||||
if data, err := coreio.Local.Read(candidate); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", coreerr.E("Executor.readLoopFile", "read file "+filePath, nil)
|
||||
}
|
||||
|
||||
// isCheckModeSafeTask reports whether a task can run without changing state
|
||||
// during check mode.
|
||||
func isCheckModeSafeTask(task *Task) bool {
|
||||
|
|
|
|||
|
|
@ -231,6 +231,40 @@ func TestExecutor_RunTaskOnHost_Good_DelegateToUsesDelegatedClient(t *testing.T)
|
|||
assert.Equal(t, 1, mock.commandCount())
|
||||
}
|
||||
|
||||
func TestExecutor_RunTaskOnHosts_Good_WithFileUsesFileContents(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, writeTestFile(joinPath(dir, "fragments", "hello.txt"), []byte("hello from file"), 0644))
|
||||
|
||||
e := NewExecutor(dir)
|
||||
mock := NewMockSSHClient()
|
||||
e.SetInventoryDirect(&Inventory{
|
||||
All: &InventoryGroup{
|
||||
Hosts: map[string]*Host{
|
||||
"host1": {},
|
||||
},
|
||||
},
|
||||
})
|
||||
e.clients["host1"] = mock
|
||||
|
||||
task := &Task{
|
||||
Name: "Read file loop",
|
||||
Module: "debug",
|
||||
Args: map[string]any{"msg": "{{ item }}"},
|
||||
Register: "debug_result",
|
||||
WithFile: []any{
|
||||
"fragments/hello.txt",
|
||||
},
|
||||
}
|
||||
|
||||
err := e.runTaskOnHosts(context.Background(), []string{"host1"}, task, &Play{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, e.results["host1"])
|
||||
require.NotNil(t, e.results["host1"]["debug_result"])
|
||||
require.Len(t, e.results["host1"]["debug_result"].Results, 1)
|
||||
assert.Equal(t, "hello from file", e.results["host1"]["debug_result"].Results[0].Msg)
|
||||
}
|
||||
|
||||
func TestExecutor_ExecuteModule_Good_ShortFormCommunityAlias(t *testing.T) {
|
||||
e := NewExecutor("/tmp")
|
||||
mock := NewMockSSHClient()
|
||||
|
|
|
|||
|
|
@ -388,6 +388,11 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Preserve with_file so the executor can resolve file contents at runtime.
|
||||
if files, ok := m["with_file"]; ok && t.WithFile == nil {
|
||||
t.WithFile = files
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
1
types.go
1
types.go
|
|
@ -114,6 +114,7 @@ type Task struct {
|
|||
// Include/import directives
|
||||
IncludeTasks string `yaml:"include_tasks,omitempty"`
|
||||
ImportTasks string `yaml:"import_tasks,omitempty"`
|
||||
WithFile any `yaml:"with_file,omitempty"`
|
||||
IncludeRole *struct {
|
||||
Name string `yaml:"name"`
|
||||
TasksFrom string `yaml:"tasks_from,omitempty"`
|
||||
|
|
|
|||
|
|
@ -204,6 +204,25 @@ with_dict:
|
|||
assert.Equal(t, "two", second["value"])
|
||||
}
|
||||
|
||||
func TestTypes_Task_UnmarshalYAML_Good_WithFile(t *testing.T) {
|
||||
input := `
|
||||
name: Read files
|
||||
debug:
|
||||
msg: "{{ item }}"
|
||||
with_file:
|
||||
- templates/a.txt
|
||||
- templates/b.txt
|
||||
`
|
||||
var task Task
|
||||
err := yaml.Unmarshal([]byte(input), &task)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, task.WithFile)
|
||||
files, ok := task.WithFile.([]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []any{"templates/a.txt", "templates/b.txt"}, files)
|
||||
}
|
||||
|
||||
func TestTypes_Task_UnmarshalYAML_Good_WithNotify(t *testing.T) {
|
||||
input := `
|
||||
name: Install package
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue