feat(ansible): add host context template vars
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
0560bccb8b
commit
9925b7d2e8
2 changed files with 135 additions and 5 deletions
98
executor.go
98
executor.go
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue