From 6f5d1659cd1a6e874b75e68153d582920f905986 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 09:52:23 +0000 Subject: [PATCH] fix(ansible): support include_tasks apply defaults --- executor.go | 3 ++- executor_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ parser.go | 1 + types.go | 19 ++++++++------- types_test.go | 24 +++++++++++++++++++ 5 files changed, 98 insertions(+), 10 deletions(-) diff --git a/executor.go b/executor.go index 5606c21..c0be94e 100644 --- a/executor.go +++ b/executor.go @@ -1938,8 +1938,9 @@ func (e *Executor) runIncludeTasks(ctx context.Context, hosts []string, task *Ta for _, t := range tasks { effectiveTask := t effectiveTask.Vars = mergeTaskVars(task.Vars, t.Vars) + e.applyRoleTaskDefaults(&effectiveTask, task.Apply) if len(effectiveTask.Vars) > 0 { - effectiveTask.Vars = e.templateArgs(effectiveTask.Vars, targetHost, task) + effectiveTask.Vars = e.templateArgs(effectiveTask.Vars, targetHost, &effectiveTask) } if len(task.Tags) > 0 { effectiveTask.Tags = mergeStringSlices(task.Tags, effectiveTask.Tags) diff --git a/executor_test.go b/executor_test.go index e1d3f44..a8add2b 100644 --- a/executor_test.go +++ b/executor_test.go @@ -12,6 +12,26 @@ import ( "gopkg.in/yaml.v3" ) +type becomeCall struct { + become bool + user string + password string +} + +type trackingMockClient struct { + *MockSSHClient + becomeCalls []becomeCall +} + +func newTrackingMockClient() *trackingMockClient { + return &trackingMockClient{MockSSHClient: NewMockSSHClient()} +} + +func (c *trackingMockClient) SetBecome(become bool, user, password string) { + c.becomeCalls = append(c.becomeCalls, becomeCall{become: become, user: user, password: password}) + c.MockSSHClient.SetBecome(become, user, password) +} + // --- NewExecutor --- func TestExecutor_NewExecutor_Good(t *testing.T) { @@ -746,6 +766,47 @@ func TestExecutor_RunIncludeRole_Good_TemplatesRoleName(t *testing.T) { assert.Equal(t, "role ran", e.results["localhost"]["role_result"].Msg) } +func TestExecutor_RunIncludeTasks_Good_AppliesTaskDefaultsToChildren(t *testing.T) { + dir := t.TempDir() + require.NoError(t, writeTestFile(joinPath(dir, "tasks", "child.yml"), []byte(`--- +- name: included shell task + shell: echo "{{ included_value }}" + register: child_result +`), 0644)) + + e := NewExecutor(dir) + e.SetInventoryDirect(&Inventory{ + All: &InventoryGroup{ + Hosts: map[string]*Host{ + "host1": {}, + }, + }, + }) + mock := newTrackingMockClient() + e.clients["host1"] = mock + become := true + mock.expectCommand(`echo "from-apply"`, "from-apply\n", "", 0) + + err := e.runTaskOnHosts(context.Background(), []string{"host1"}, &Task{ + Name: "include child tasks", + IncludeTasks: "tasks/child.yml", + Apply: &TaskApply{ + Vars: map[string]any{ + "included_value": "from-apply", + }, + Become: &become, + BecomeUser: "root", + }, + }, &Play{}) + require.NoError(t, err) + + require.NotNil(t, e.results["host1"]["child_result"]) + assert.Equal(t, "from-apply\n", e.results["host1"]["child_result"].Stdout) + assert.True(t, mock.hasExecuted(`echo "from-apply"`)) + require.NotEmpty(t, mock.becomeCalls) + assert.Contains(t, mock.becomeCalls, becomeCall{become: true, user: "root", password: ""}) +} + func TestExecutor_RunPlay_Good_AppliesPlayTagsToTasks(t *testing.T) { e := NewExecutor("/tmp") e.Tags = []string{"deploy"} diff --git a/parser.go b/parser.go index 8982612..52fcf52 100644 --- a/parser.go +++ b/parser.go @@ -427,6 +427,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { "retries": true, "delay": true, "until": true, "action": true, "local_action": true, "include_tasks": true, "import_tasks": true, + "apply": true, "include_role": true, "import_role": true, "public": 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, } diff --git a/types.go b/types.go index 4729717..5550645 100644 --- a/types.go +++ b/types.go @@ -122,15 +122,16 @@ 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"` - WithSubelements any `yaml:"with_subelements,omitempty"` - IncludeRole *RoleRef `yaml:"include_role,omitempty"` - ImportRole *RoleRef `yaml:"import_role,omitempty"` + IncludeTasks string `yaml:"include_tasks,omitempty"` + ImportTasks string `yaml:"import_tasks,omitempty"` + Apply *TaskApply `yaml:"apply,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 *RoleRef `yaml:"include_role,omitempty"` + ImportRole *RoleRef `yaml:"import_role,omitempty"` // Raw YAML for module extraction raw map[string]any diff --git a/types_test.go b/types_test.go index af2de2b..e838c78 100644 --- a/types_test.go +++ b/types_test.go @@ -605,6 +605,30 @@ include_tasks: other-tasks.yml assert.Equal(t, "other-tasks.yml", task.IncludeTasks) } +func TestTypes_Task_UnmarshalYAML_Good_IncludeTasksApply(t *testing.T) { + input := ` +name: Include tasks +include_tasks: other-tasks.yml +apply: + tags: + - deploy + become: true + become_user: root + environment: + APP_ENV: production +` + var task Task + err := yaml.Unmarshal([]byte(input), &task) + + require.NoError(t, err) + require.NotNil(t, task.Apply) + assert.Equal(t, []string{"deploy"}, task.Apply.Tags) + require.NotNil(t, task.Apply.Become) + assert.True(t, *task.Apply.Become) + assert.Equal(t, "root", task.Apply.BecomeUser) + assert.Equal(t, "production", task.Apply.Environment["APP_ENV"]) +} + func TestTypes_Task_UnmarshalYAML_Good_IncludeRole(t *testing.T) { input := ` name: Include role