From f80825783c4d62c38cd89b4dce6615608b2bee55 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 20:54:36 +0000 Subject: [PATCH] fix(ansible): support import_playbook expansion --- parser.go | 26 +++++++++++++++++++++++++- parser_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 1 + 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/parser.go b/parser.go index 2489bf6..be5de1e 100644 --- a/parser.go +++ b/parser.go @@ -38,6 +38,17 @@ func NewParser(basePath string) *Parser { // // plays, err := parser.ParsePlaybook("/workspace/playbooks/site.yml") func (p *Parser) ParsePlaybook(path string) ([]Play, error) { + return p.parsePlaybook(path, make(map[string]bool)) +} + +func (p *Parser) parsePlaybook(path string, seen map[string]bool) ([]Play, error) { + cleanedPath := cleanPath(path) + if seen[cleanedPath] { + return nil, coreerr.E("Parser.parsePlaybook", "circular import_playbook detected: "+cleanedPath, nil) + } + seen[cleanedPath] = true + defer delete(seen, cleanedPath) + data, err := coreio.Local.Read(path) if err != nil { return nil, coreerr.E("Parser.ParsePlaybook", "read playbook", err) @@ -48,14 +59,27 @@ func (p *Parser) ParsePlaybook(path string) ([]Play, error) { return nil, coreerr.E("Parser.ParsePlaybook", "parse playbook", err) } + var expanded []Play + // Process each play for i := range plays { + if plays[i].ImportPlaybook != "" { + importPath := joinPath(pathDir(path), plays[i].ImportPlaybook) + imported, err := p.parsePlaybook(importPath, seen) + if err != nil { + return nil, coreerr.E("Parser.ParsePlaybook", sprintf("expand import_playbook %d", i), err) + } + expanded = append(expanded, imported...) + continue + } + if err := p.processPlay(&plays[i]); err != nil { return nil, coreerr.E("Parser.ParsePlaybook", sprintf("process play %d", i), err) } + expanded = append(expanded, plays[i]) } - return plays, nil + return expanded, nil } // ParsePlaybookIter returns an iterator for plays in an Ansible playbook file. diff --git a/parser_test.go b/parser_test.go index 96725c3..6181207 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1,6 +1,7 @@ package ansible import ( + "os" "testing" "github.com/stretchr/testify/assert" @@ -74,6 +75,54 @@ func TestParser_ParsePlaybook_Good_MultiplePlays(t *testing.T) { assert.Equal(t, "local", plays[1].Connection) } +func TestParser_ParsePlaybook_Good_ImportPlaybook(t *testing.T) { + dir := t.TempDir() + mainPath := joinPath(dir, "site.yml") + importDir := joinPath(dir, "plays") + importPath := joinPath(importDir, "web.yml") + + yamlMain := `--- +- name: Before import + hosts: all + tasks: + - name: Say before + debug: + msg: "before" + +- import_playbook: plays/web.yml + +- name: After import + hosts: all + tasks: + - name: Say after + debug: + msg: "after" +` + yamlImported := `--- +- name: Imported play + hosts: webservers + 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, 3) + assert.Equal(t, "Before import", plays[0].Name) + assert.Equal(t, "Imported play", plays[1].Name) + assert.Equal(t, "After import", plays[2].Name) + assert.Equal(t, "webservers", plays[1].Hosts) + assert.Len(t, plays[1].Tasks, 1) + assert.Equal(t, "Say imported", plays[1].Tasks[0].Name) +} + 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 f34b65e..e772895 100644 --- a/types.go +++ b/types.go @@ -21,6 +21,7 @@ type Playbook struct { type Play struct { Name string `yaml:"name"` Hosts string `yaml:"hosts"` + ImportPlaybook string `yaml:"import_playbook,omitempty"` Connection string `yaml:"connection,omitempty"` Become bool `yaml:"become,omitempty"` BecomeUser string `yaml:"become_user,omitempty"`