From 84451b2bd83be63651bf9ac5c0aca2eb7caa62f5 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 02:07:09 +0000 Subject: [PATCH] feat(ansible): support top-level inventory groups Co-authored-by: Virgil --- parser_test.go | 35 +++++++++++++++++ types.go | 100 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/parser_test.go b/parser_test.go index 7a5af56..e7432fc 100644 --- a/parser_test.go +++ b/parser_test.go @@ -525,6 +525,41 @@ all: assert.Len(t, inv.All.Children["databases"].Hosts, 1) } +func TestParser_ParseInventory_Good_TopLevelGroups(t *testing.T) { + dir := t.TempDir() + path := joinPath(dir, "inventory.yml") + + yaml := `--- +webservers: + vars: + tier: web + hosts: + web1: + ansible_host: 10.0.0.1 + web2: + ansible_host: 10.0.0.2 +databases: + hosts: + db1: + ansible_host: 10.0.1.1 +` + require.NoError(t, writeTestFile(path, []byte(yaml), 0644)) + + p := NewParser(dir) + inv, err := p.ParseInventory(path) + + require.NoError(t, err) + require.NotNil(t, inv.All) + require.NotNil(t, inv.All.Children["webservers"]) + require.NotNil(t, inv.All.Children["databases"]) + assert.Len(t, inv.All.Children["webservers"].Hosts, 2) + assert.Len(t, inv.All.Children["databases"].Hosts, 1) + assert.Equal(t, "web", inv.All.Children["webservers"].Vars["tier"]) + assert.ElementsMatch(t, []string{"web1", "web2", "db1"}, GetHosts(inv, "all")) + assert.Equal(t, []string{"web1", "web2"}, GetHosts(inv, "webservers")) + assert.Equal(t, "web", GetHostVars(inv, "web1")["tier"]) +} + func TestParser_ParseInventory_Good_WithVars(t *testing.T) { dir := t.TempDir() path := joinPath(dir, "inventory.yml") diff --git a/types.go b/types.go index 295743d..1ca06ab 100644 --- a/types.go +++ b/types.go @@ -2,6 +2,9 @@ package ansible import ( "time" + + coreerr "dappco.re/go/core/log" + "gopkg.in/yaml.v3" ) // Playbook represents an Ansible playbook. @@ -205,6 +208,103 @@ type Inventory struct { All *InventoryGroup `yaml:"all"` } +// UnmarshalYAML supports both the explicit `all:` root and inventories that +// declare top-level groups directly. +func (i *Inventory) UnmarshalYAML(unmarshal func(any) error) error { + var raw map[string]any + if err := unmarshal(&raw); err != nil { + return err + } + + root := &InventoryGroup{} + rootInput := make(map[string]any) + if all, ok := raw["all"]; ok { + group, err := decodeInventoryGroupValue(all) + if err != nil { + return coreerr.E("Inventory.UnmarshalYAML", "decode all group", err) + } + root = group + } + + for name, value := range raw { + if name == "all" { + continue + } + + switch name { + case "hosts", "children", "vars": + rootInput[name] = value + continue + } + + group, err := decodeInventoryGroupValue(value) + if err != nil { + return coreerr.E("Inventory.UnmarshalYAML", "decode group "+name, err) + } + + if root.Children == nil { + root.Children = make(map[string]*InventoryGroup) + } + root.Children[name] = group + } + + if len(rootInput) > 0 { + extra, err := decodeInventoryGroupValue(rootInput) + if err != nil { + return coreerr.E("Inventory.UnmarshalYAML", "decode root group", err) + } + mergeInventoryGroups(root, extra) + } + + i.All = root + return nil +} + +func decodeInventoryGroupValue(value any) (*InventoryGroup, error) { + if value == nil { + return &InventoryGroup{}, nil + } + + data, err := yaml.Marshal(value) + if err != nil { + return nil, err + } + + var group InventoryGroup + if err := yaml.Unmarshal(data, &group); err != nil { + return nil, err + } + + return &group, nil +} + +func mergeInventoryGroups(dst, src *InventoryGroup) { + if dst == nil || src == nil { + return + } + + if dst.Hosts == nil && len(src.Hosts) > 0 { + dst.Hosts = make(map[string]*Host, len(src.Hosts)) + } + for name, host := range src.Hosts { + dst.Hosts[name] = host + } + + if dst.Children == nil && len(src.Children) > 0 { + dst.Children = make(map[string]*InventoryGroup, len(src.Children)) + } + for name, child := range src.Children { + dst.Children[name] = child + } + + if dst.Vars == nil && len(src.Vars) > 0 { + dst.Vars = make(map[string]any, len(src.Vars)) + } + for key, value := range src.Vars { + dst.Vars[key] = value + } +} + // InventoryGroup represents a group in inventory. // // Example: