Templated include task paths per host

This commit is contained in:
Virgil 2026-04-01 21:57:05 +00:00
parent 87cd890ea1
commit c77a32f24f
2 changed files with 81 additions and 9 deletions

View file

@ -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
}
}
}

View file

@ -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{