diff --git a/executor.go b/executor.go index b8610e3..bbd4dd5 100644 --- a/executor.go +++ b/executor.go @@ -1067,17 +1067,35 @@ func (e *Executor) runIncludeTasks(ctx context.Context, hosts []string, task *Ta path = task.ImportTasks } - // Resolve path relative to playbook - path = e.templateString(path, "", nil) - - tasks, err := e.parser.ParseTasks(path) - if err != nil { - return coreerr.E("Executor.runIncludeTasks", "include_tasks "+path, err) + if path == "" || len(hosts) == 0 { + return nil } - for _, t := range tasks { - if err := e.runTaskOnHosts(ctx, hosts, &t, play); err != nil { - return err + // Resolve the include path per host so host-specific vars can select a + // different task file for each target. + hostsByPath := make(map[string][]string) + pathOrder := make([]string, 0, len(hosts)) + for _, host := range hosts { + resolvedPath := e.templateString(path, host, task) + if resolvedPath == "" { + continue + } + if _, ok := hostsByPath[resolvedPath]; !ok { + pathOrder = append(pathOrder, resolvedPath) + } + hostsByPath[resolvedPath] = append(hostsByPath[resolvedPath], host) + } + + for _, resolvedPath := range pathOrder { + tasks, err := e.parser.ParseTasks(resolvedPath) + if err != nil { + return coreerr.E("Executor.runIncludeTasks", "include_tasks "+resolvedPath, err) + } + + for _, t := range tasks { + if err := e.runTaskOnHosts(ctx, hostsByPath[resolvedPath], &t, play); err != nil { + return err + } } } diff --git a/executor_extra_test.go b/executor_extra_test.go index 9aa0718..a79e53d 100644 --- a/executor_extra_test.go +++ b/executor_extra_test.go @@ -657,6 +657,60 @@ func TestExecutorExtra_RunIncludeTasks_Good_RelativePath(t *testing.T) { assert.Contains(t, started, "localhost:Included second task") } +func TestExecutorExtra_RunIncludeTasks_Good_HostSpecificTemplate(t *testing.T) { + dir := t.TempDir() + + require.NoError(t, writeTestFile(joinPath(dir, "web.yml"), []byte(`- name: Web included task + debug: + msg: web +`), 0644)) + require.NoError(t, writeTestFile(joinPath(dir, "db.yml"), []byte(`- name: DB included task + debug: + msg: db +`), 0644)) + + gatherFacts := false + play := &Play{ + Name: "Include host-specific tasks", + Hosts: "all", + Connection: "local", + GatherFacts: &gatherFacts, + } + + e := NewExecutor(dir) + e.SetInventoryDirect(&Inventory{ + All: &InventoryGroup{ + Hosts: map[string]*Host{ + "web1": { + AnsibleConnection: "local", + Vars: map[string]any{ + "include_file": "web.yml", + }, + }, + "db1": { + AnsibleConnection: "local", + Vars: map[string]any{ + "include_file": "db.yml", + }, + }, + }, + }, + }) + + var started []string + e.OnTaskStart = func(host string, task *Task) { + started = append(started, host+":"+task.Name) + } + + require.NoError(t, e.runTaskOnHosts(context.Background(), []string{"web1", "db1"}, &Task{ + Name: "Load host-specific tasks", + IncludeTasks: "{{ include_file }}", + }, play)) + + assert.Contains(t, started, "web1:Web included task") + assert.Contains(t, started, "db1:DB included task") +} + func TestExecutorExtra_GetHostsIter_Good(t *testing.T) { inv := &Inventory{ All: &InventoryGroup{