feat(ansible): add host context template vars

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 14:01:48 +00:00
parent 0560bccb8b
commit 9925b7d2e8
2 changed files with 135 additions and 5 deletions

View file

@ -6,6 +6,7 @@ import (
"errors"
"io"
"io/fs"
"maps"
"path"
"path/filepath"
"reflect"
@ -190,6 +191,82 @@ func (e *Executor) hostScopedVars(host string) map[string]any {
return cloned
}
func inventoryHostnameShort(host string) string {
host = corexTrimSpace(host)
if host == "" {
return ""
}
short, _, ok := strings.Cut(host, ".")
if ok && short != "" {
return short
}
return host
}
func (e *Executor) hostMagicVars(host string) map[string]any {
values := map[string]any{
"inventory_hostname": host,
"inventory_hostname_short": inventoryHostnameShort(host),
}
if e != nil && e.inventory != nil {
if groupNames := hostGroupNames(e.inventory.All, host); len(groupNames) > 0 {
values["group_names"] = groupNames
}
}
if e != nil {
if facts, ok := e.facts[host]; ok {
values["ansible_facts"] = factsToMap(facts)
}
}
return values
}
func hostGroupNames(group *InventoryGroup, host string) []string {
if group == nil || host == "" {
return nil
}
names := make(map[string]bool)
collectHostGroupNames(group, host, "", names)
if len(names) == 0 {
return nil
}
result := make([]string, 0, len(names))
for name := range names {
result = append(result, name)
}
slices.Sort(result)
return result
}
func collectHostGroupNames(group *InventoryGroup, host, name string, names map[string]bool) bool {
if group == nil {
return false
}
found := false
if _, ok := group.Hosts[host]; ok {
found = true
}
childNames := slices.Sorted(maps.Keys(group.Children))
for _, childName := range childNames {
if collectHostGroupNames(group.Children[childName], host, childName, names) {
found = true
}
}
if found && name != "" {
names[name] = true
}
return found
}
// Run executes a playbook.
//
// Example:
@ -2431,6 +2508,10 @@ func isConditionBoundary(ch byte) bool {
func (e *Executor) lookupConditionValue(name string, host string, task *Task, locals map[string]any) (any, bool) {
name = corexTrimSpace(name)
if value, ok := e.hostMagicVars(host)[name]; ok {
return value, true
}
if locals != nil {
if val, ok := locals[name]; ok {
return val, true
@ -2479,13 +2560,10 @@ func (e *Executor) lookupConditionValue(name string, host string, task *Task, lo
}
}
if name == "ansible_facts" {
if facts, ok := e.facts[host]; ok {
if facts, ok := e.facts[host]; ok {
if name == "ansible_facts" {
return factsToMap(facts), true
}
}
if facts, ok := e.facts[host]; ok {
switch name {
case "ansible_hostname":
return facts.Hostname, true
@ -2515,6 +2593,12 @@ func (e *Executor) lookupConditionValue(name string, host string, task *Task, lo
base := parts[0]
path := parts[1]
if magic, ok := e.hostMagicVars(host)[base]; ok {
if nested, ok := lookupNestedValue(magic, path); ok {
return nested, true
}
}
if locals != nil {
if val, ok := locals[base]; ok {
if nested, ok := lookupNestedValue(val, path); ok {
@ -2707,6 +2791,10 @@ func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
}
}
if value, ok := e.hostMagicVars(host)[expr]; ok {
return sprintf("%v", value)
}
// Resolve nested maps from vars, task vars, or host vars.
if contains(expr, ".") {
parts := splitN(expr, ".", 2)

View file

@ -1843,6 +1843,48 @@ func TestExecutor_TemplateString_Good_NoTemplate(t *testing.T) {
assert.Equal(t, "plain string", result)
}
func TestExecutor_TemplateString_Good_InventoryHostnameShort(t *testing.T) {
e := NewExecutor("/tmp")
result := e.templateString("{{ inventory_hostname_short }}", "web01.example.com", nil)
assert.Equal(t, "web01", result)
}
func TestExecutor_TemplateString_Good_GroupNames(t *testing.T) {
e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{
Children: map[string]*InventoryGroup{
"production": {
Hosts: map[string]*Host{
"web01.example.com": {},
},
},
"web": {
Children: map[string]*InventoryGroup{
"frontend": {
Hosts: map[string]*Host{
"web01.example.com": {},
},
},
},
},
},
},
})
result := e.templateString("{{ group_names }}", "web01.example.com", nil)
assert.Equal(t, "[frontend production web]", result)
}
func TestExecutor_EvalCondition_Good_InventoryHostnameShort(t *testing.T) {
e := NewExecutor("/tmp")
assert.True(t, e.evalCondition("inventory_hostname_short == 'web01'", "web01.example.com"))
}
// --- applyFilter ---
func TestExecutor_ApplyFilter_Good_Default(t *testing.T) {