diff --git a/parser.go b/parser.go index 52fcf52..9e24500 100644 --- a/parser.go +++ b/parser.go @@ -427,11 +427,36 @@ 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, + "ansible.builtin.include_tasks": true, "ansible.legacy.include_tasks": true, + "ansible.builtin.import_tasks": true, "ansible.legacy.import_tasks": true, "apply": true, "include_role": true, "import_role": true, "public": true, + "ansible.builtin.include_role": true, "ansible.legacy.include_role": true, + "ansible.builtin.import_role": true, "ansible.legacy.import_role": 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, } + if value, ok := directiveValue(m, "include_tasks"); ok && t.IncludeTasks == "" { + t.IncludeTasks = sprintf("%v", value) + } + if value, ok := directiveValue(m, "import_tasks"); ok && t.ImportTasks == "" { + t.ImportTasks = sprintf("%v", value) + } + if value, ok := directiveValue(m, "include_role"); ok && t.IncludeRole == nil { + var ref RoleRef + if err := decodeYAMLValue(value, &ref); err != nil { + return err + } + t.IncludeRole = &ref + } + if value, ok := directiveValue(m, "import_role"); ok && t.ImportRole == nil { + var ref RoleRef + if err := decodeYAMLValue(value, &ref); err != nil { + return err + } + t.ImportRole = &ref + } + for key, val := range m { if knownKeys[key] { continue @@ -561,6 +586,14 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { return nil } +func decodeYAMLValue(value any, out any) error { + data, err := yaml.Marshal(value) + if err != nil { + return err + } + return yaml.Unmarshal(data, out) +} + // parseActionSpec converts action/local_action values into a module name and // argument map. func parseActionSpec(value any) (string, map[string]any) { diff --git a/parser_test.go b/parser_test.go index 2f82044..bdb853d 100644 --- a/parser_test.go +++ b/parser_test.go @@ -155,6 +155,36 @@ func TestParser_ParsePlaybook_Good_TemplatedImportPlaybook(t *testing.T) { assert.Equal(t, "all", plays[0].Hosts) } +func TestParser_ParsePlaybook_Good_FQCNImportPlaybook(t *testing.T) { + dir := t.TempDir() + mainPath := joinPath(dir, "site.yml") + importDir := joinPath(dir, "plays") + importPath := joinPath(importDir, "web.yml") + + yamlMain := `--- +- ansible.builtin.import_playbook: plays/web.yml +` + yamlImported := `--- +- name: Imported play + hosts: all + tasks: + - name: Say imported + debug: + msg: "imported" +` + require.NoError(t, os.MkdirAll(importDir, 0755)) + require.NoError(t, writeTestFile(mainPath, []byte(yamlMain), 0644)) + require.NoError(t, writeTestFile(importPath, []byte(yamlImported), 0644)) + + p := NewParser(dir) + plays, err := p.ParsePlaybook(mainPath) + + require.NoError(t, err) + require.Len(t, plays, 1) + assert.Equal(t, "Imported play", plays[0].Name) + assert.Equal(t, "all", plays[0].Hosts) +} + func TestParser_ParsePlaybook_Good_WithVars(t *testing.T) { dir := t.TempDir() path := joinPath(dir, "playbook.yml") diff --git a/types.go b/types.go index 2c2752b..f11e4a2 100644 --- a/types.go +++ b/types.go @@ -44,6 +44,27 @@ type Play struct { MaxFailPercent int `yaml:"max_fail_percentage,omitempty"` } +// UnmarshalYAML handles play-level aliases such as ansible.builtin.import_playbook. +func (p *Play) UnmarshalYAML(node *yaml.Node) error { + type rawPlay Play + var raw rawPlay + if err := node.Decode(&raw); err != nil { + return err + } + *p = Play(raw) + + var fields map[string]any + if err := node.Decode(&fields); err != nil { + return err + } + + if value, ok := directiveValue(fields, "import_playbook"); ok && p.ImportPlaybook == "" { + p.ImportPlaybook = sprintf("%v", value) + } + + return nil +} + // RoleRef represents a role reference in a play. // // Example: @@ -90,6 +111,20 @@ func (r *RoleRef) UnmarshalYAML(unmarshal func(any) error) error { return nil } +func directiveValue(fields map[string]any, name string) (any, bool) { + if fields == nil { + return nil, false + } + + for _, key := range []string{name, "ansible.builtin." + name, "ansible.legacy." + name} { + if value, ok := fields[key]; ok { + return value, true + } + } + + return nil, false +} + // Task represents an Ansible task. // // Example: diff --git a/types_test.go b/types_test.go index d51149c..90815dd 100644 --- a/types_test.go +++ b/types_test.go @@ -605,6 +605,18 @@ include_tasks: other-tasks.yml assert.Equal(t, "other-tasks.yml", task.IncludeTasks) } +func TestTypes_Task_UnmarshalYAML_Good_IncludeTasksFQCN(t *testing.T) { + input := ` +name: Include tasks +ansible.builtin.include_tasks: other-tasks.yml +` + var task Task + err := yaml.Unmarshal([]byte(input), &task) + + require.NoError(t, err) + assert.Equal(t, "other-tasks.yml", task.IncludeTasks) +} + func TestTypes_Task_UnmarshalYAML_Good_IncludeTasksApply(t *testing.T) { input := ` name: Include tasks @@ -681,6 +693,22 @@ include_role: common assert.Equal(t, "common", task.IncludeRole.Role) } +func TestTypes_Task_UnmarshalYAML_Good_IncludeRoleFQCN(t *testing.T) { + input := ` +name: Include role +ansible.builtin.include_role: + name: common + tasks_from: setup.yml +` + var task Task + err := yaml.Unmarshal([]byte(input), &task) + + require.NoError(t, err) + require.NotNil(t, task.IncludeRole) + assert.Equal(t, "common", task.IncludeRole.Role) + assert.Equal(t, "setup.yml", task.IncludeRole.TasksFrom) +} + func TestTypes_Task_UnmarshalYAML_Good_ImportRoleStringForm(t *testing.T) { input := ` name: Import role @@ -694,6 +722,19 @@ import_role: common assert.Equal(t, "common", task.ImportRole.Role) } +func TestTypes_Task_UnmarshalYAML_Good_ImportRoleFQCN(t *testing.T) { + input := ` +name: Import role +ansible.builtin.import_role: common +` + var task Task + err := yaml.Unmarshal([]byte(input), &task) + + require.NoError(t, err) + require.NotNil(t, task.ImportRole) + assert.Equal(t, "common", task.ImportRole.Role) +} + func TestTypes_Task_UnmarshalYAML_Good_BecomeFields(t *testing.T) { input := ` name: Privileged task