From 3a4118ada8798d3bcdadfc3481cf7901555186bd Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 22:00:12 +0000 Subject: [PATCH] Fix include task variable inheritance --- executor.go | 22 +++++++++++-- executor_extra_test.go | 72 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/executor.go b/executor.go index bbd4dd5..07e8e25 100644 --- a/executor.go +++ b/executor.go @@ -1093,7 +1093,9 @@ func (e *Executor) runIncludeTasks(ctx context.Context, hosts []string, task *Ta } for _, t := range tasks { - if err := e.runTaskOnHosts(ctx, hostsByPath[resolvedPath], &t, play); err != nil { + effectiveTask := t + effectiveTask.Vars = mergeTaskVars(task.Vars, t.Vars) + if err := e.runTaskOnHosts(ctx, hostsByPath[resolvedPath], &effectiveTask, play); err != nil { return err } } @@ -1120,12 +1122,28 @@ func (e *Executor) runIncludeRole(ctx context.Context, hosts []string, task *Tas roleRef := &RoleRef{ Role: roleName, TasksFrom: tasksFrom, - Vars: roleVars, + Vars: mergeTaskVars(roleVars, task.Vars), } return e.runRole(ctx, hosts, roleRef, play) } +// mergeTaskVars combines include-task vars with child task vars. +func mergeTaskVars(parent, child map[string]any) map[string]any { + if len(parent) == 0 && len(child) == 0 { + return nil + } + + merged := make(map[string]any, len(parent)+len(child)) + for k, v := range parent { + merged[k] = v + } + for k, v := range child { + merged[k] = v + } + return merged +} + // getHosts returns hosts matching the pattern. func (e *Executor) getHosts(pattern string) []string { if e.inventory == nil { diff --git a/executor_extra_test.go b/executor_extra_test.go index a79e53d..ef0d4e4 100644 --- a/executor_extra_test.go +++ b/executor_extra_test.go @@ -657,6 +657,37 @@ func TestExecutorExtra_RunIncludeTasks_Good_RelativePath(t *testing.T) { assert.Contains(t, started, "localhost:Included second task") } +func TestExecutorExtra_RunIncludeTasks_Good_InheritsTaskVars(t *testing.T) { + dir := t.TempDir() + includedPath := joinPath(dir, "included-vars.yml") + yaml := `- name: Included var task + debug: + msg: "{{ include_message }}" + register: included_result +` + require.NoError(t, writeTestFile(includedPath, []byte(yaml), 0644)) + + gatherFacts := false + play := &Play{ + Name: "Include vars", + Hosts: "localhost", + GatherFacts: &gatherFacts, + Tasks: []Task{ + { + Name: "Load included tasks", + IncludeTasks: "included-vars.yml", + Vars: map[string]any{"include_message": "hello from include"}, + }, + }, + } + + e := NewExecutor(dir) + require.NoError(t, e.runPlay(context.Background(), play)) + + require.NotNil(t, e.results["localhost"]["included_result"]) + assert.Equal(t, "hello from include", e.results["localhost"]["included_result"].Msg) +} + func TestExecutorExtra_RunIncludeTasks_Good_HostSpecificTemplate(t *testing.T) { dir := t.TempDir() @@ -711,6 +742,47 @@ func TestExecutorExtra_RunIncludeTasks_Good_HostSpecificTemplate(t *testing.T) { assert.Contains(t, started, "db1:DB included task") } +func TestExecutorExtra_RunIncludeRole_Good_InheritsTaskVars(t *testing.T) { + dir := t.TempDir() + require.NoError(t, writeTestFile(joinPath(dir, "roles", "demo", "tasks", "main.yml"), []byte(`--- +- name: Role var task + debug: + msg: "{{ role_message }}" + register: role_result +`), 0644)) + + e := NewExecutor(dir) + e.SetInventoryDirect(&Inventory{ + All: &InventoryGroup{ + Hosts: map[string]*Host{ + "localhost": {}, + }, + }, + }) + + gatherFacts := false + play := &Play{ + Hosts: "localhost", + Connection: "local", + GatherFacts: &gatherFacts, + } + + require.NoError(t, e.runTaskOnHosts(context.Background(), []string{"localhost"}, &Task{ + Name: "Load role", + IncludeRole: &struct { + Name string `yaml:"name"` + TasksFrom string `yaml:"tasks_from,omitempty"` + Vars map[string]any `yaml:"vars,omitempty"` + }{ + Name: "demo", + }, + Vars: map[string]any{"role_message": "hello from role"}, + }, play)) + + require.NotNil(t, e.results["localhost"]["role_result"]) + assert.Equal(t, "hello from role", e.results["localhost"]["role_result"].Msg) +} + func TestExecutorExtra_GetHostsIter_Good(t *testing.T) { inv := &Inventory{ All: &InventoryGroup{