diff --git a/parser.go b/parser.go index 86cb119..aba3a2f 100644 --- a/parser.go +++ b/parser.go @@ -516,66 +516,69 @@ func hasHost(group *InventoryGroup, name string) bool { func GetHostVars(inv *Inventory, hostname string) map[string]any { vars := make(map[string]any) - // Collect vars from all levels - collectHostVars(inv.All, hostname, vars) + if inv == nil || inv.All == nil { + return vars + } + + path, host, ok := findHostPath(inv.All, hostname) + if !ok { + return vars + } + + // Merge vars from outermost to innermost so nearer scopes win. + for _, group := range path { + for k, v := range group.Vars { + vars[k] = v + } + } + + // Host connection settings and inline vars override group vars. + if host != nil { + if host.AnsibleHost != "" { + vars["ansible_host"] = host.AnsibleHost + } + if host.AnsiblePort != 0 { + vars["ansible_port"] = host.AnsiblePort + } + if host.AnsibleUser != "" { + vars["ansible_user"] = host.AnsibleUser + } + if host.AnsiblePassword != "" { + vars["ansible_password"] = host.AnsiblePassword + } + if host.AnsibleSSHPrivateKeyFile != "" { + vars["ansible_ssh_private_key_file"] = host.AnsibleSSHPrivateKeyFile + } + if host.AnsibleConnection != "" { + vars["ansible_connection"] = host.AnsibleConnection + } + if host.AnsibleBecomePassword != "" { + vars["ansible_become_password"] = host.AnsibleBecomePassword + } + for k, v := range host.Vars { + vars[k] = v + } + } return vars } -func collectHostVars(group *InventoryGroup, hostname string, vars map[string]any) bool { +func findHostPath(group *InventoryGroup, hostname string) ([]*InventoryGroup, *Host, bool) { if group == nil { - return false + return nil, nil, false } - // Check if host is in this group - found := false if host, ok := group.Hosts[hostname]; ok { - found = true - // Apply group vars first - for k, v := range group.Vars { - vars[k] = v - } - // Then host vars - if host != nil { - if host.AnsibleHost != "" { - vars["ansible_host"] = host.AnsibleHost - } - if host.AnsiblePort != 0 { - vars["ansible_port"] = host.AnsiblePort - } - if host.AnsibleUser != "" { - vars["ansible_user"] = host.AnsibleUser - } - if host.AnsiblePassword != "" { - vars["ansible_password"] = host.AnsiblePassword - } - if host.AnsibleSSHPrivateKeyFile != "" { - vars["ansible_ssh_private_key_file"] = host.AnsibleSSHPrivateKeyFile - } - if host.AnsibleConnection != "" { - vars["ansible_connection"] = host.AnsibleConnection - } - if host.AnsibleBecomePassword != "" { - vars["ansible_become_password"] = host.AnsibleBecomePassword - } - for k, v := range host.Vars { - vars[k] = v - } + return []*InventoryGroup{group}, host, true + } + + for _, name := range slices.Sorted(maps.Keys(group.Children)) { + child := group.Children[name] + path, host, ok := findHostPath(child, hostname) + if ok { + return append([]*InventoryGroup{group}, path...), host, true } } - // Check children - for _, child := range group.Children { - if collectHostVars(child, hostname, vars) { - // Apply this group's vars (parent vars) - for k, v := range group.Vars { - if _, exists := vars[k]; !exists { - vars[k] = v - } - } - found = true - } - } - - return found + return nil, nil, false } diff --git a/parser_test.go b/parser_test.go index fdc3a45..141ea4a 100644 --- a/parser_test.go +++ b/parser_test.go @@ -716,6 +716,55 @@ func TestParser_GetHostVars_Good_InheritedGroupVars(t *testing.T) { assert.Equal(t, "prod", vars["env"]) } +func TestParser_GetHostVars_Good_SiblingVarsDoNotLeak(t *testing.T) { + inv := &Inventory{ + All: &InventoryGroup{ + Children: map[string]*InventoryGroup{ + "a-group": { + Vars: map[string]any{"leaked": "from-a"}, + }, + "b-group": { + Vars: map[string]any{"env": "prod"}, + Hosts: map[string]*Host{ + "prod1": { + AnsibleHost: "10.0.0.2", + }, + }, + }, + }, + }, + } + + vars := GetHostVars(inv, "prod1") + assert.Equal(t, "10.0.0.2", vars["ansible_host"]) + assert.Equal(t, "prod", vars["env"]) + assert.NotContains(t, vars, "leaked") +} + +func TestParser_GetHostVars_Good_NearestScopeWins(t *testing.T) { + inv := &Inventory{ + All: &InventoryGroup{ + Vars: map[string]any{"shared": "root"}, + Children: map[string]*InventoryGroup{ + "group": { + Vars: map[string]any{"shared": "group"}, + Hosts: map[string]*Host{ + "app1": { + AnsibleHost: "10.0.0.3", + AnsibleUser: "deploy", + Vars: map[string]any{"shared": "host"}, + }, + }, + }, + }, + }, + } + + vars := GetHostVars(inv, "app1") + assert.Equal(t, "host", vars["shared"]) + assert.Equal(t, "deploy", vars["ansible_user"]) +} + func TestParser_GetHostVars_Good_HostNotFound(t *testing.T) { inv := &Inventory{ All: &InventoryGroup{