goherence/internal/parser/inventory.go
2026-09-11 10:17:25 +03:00

204 lines
6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package parser
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
// rawGroup — форма группы прямо как она лежит в YAML-инвентаре.
type rawGroup struct {
Hosts map[string]map[string]interface{} `yaml:"hosts,omitempty"`
Vars map[string]interface{} `yaml:"vars,omitempty"`
Children map[string]rawGroup `yaml:"children,omitempty"`
}
// LoadInventory читает YAML-инвентарь (в стиле `all: children: ...`)
// и разворачивает его в плоскую карту групп с явным списком children.
func LoadInventory(path string) (*Inventory, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("читаю инвентарь: %w", err)
}
var root map[string]rawGroup
if err := yaml.Unmarshal(data, &root); err != nil {
return nil, fmt.Errorf("парсинг инвентаря: %w", err)
}
inv := &Inventory{
Groups: map[string]*Group{},
Dir: filepath.Dir(path),
}
for name, rg := range root {
flatten(inv, name, rg)
}
return inv, nil
}
// flatten рекурсивно разворачивает rawGroup (с вложенными children)
// в плоский inv.Groups, попутно строя список Children для каждой группы —
// это то дерево, по которому потом считается порядок наложения group_vars.
func flatten(inv *Inventory, name string, rg rawGroup) {
group, ok := inv.Groups[name]
if !ok {
group = &Group{Name: name, Hosts: map[string]*Host{}, Vars: map[string]interface{}{}}
inv.Groups[name] = group
}
for k, v := range rg.Vars {
group.Vars[k] = v
}
for hostName, hostVars := range rg.Hosts {
h, ok := group.Hosts[hostName]
if !ok {
h = &Host{Name: hostName, Vars: map[string]interface{}{}}
group.Hosts[hostName] = h
}
for k, v := range hostVars {
h.Vars[k] = v
}
}
for childName, childGroup := range rg.Children {
group.Children = append(group.Children, childName)
flatten(inv, childName, childGroup)
}
}
// GroupsOf возвращает имена всех групп, в которые входит хост,
// включая родительские группы через children (транзитивно).
func (inv *Inventory) GroupsOf(hostName string) []string {
seen := map[string]bool{}
for name, g := range inv.Groups {
if _, ok := g.Hosts[hostName]; ok {
markAncestors(inv, name, seen)
}
}
out := make([]string, 0, len(seen))
for name := range seen {
out = append(out, name)
}
return out
}
// markAncestors помечает саму группу и всех её "родителей" (группы,
// у которых она указана в children) как содержащие данный хост.
func markAncestors(inv *Inventory, group string, seen map[string]bool) {
if seen[group] {
return
}
seen[group] = true
for name, g := range inv.Groups {
for _, child := range g.Children {
if child == group {
markAncestors(inv, name, seen)
}
}
}
}
// depth — расстояние группы от "all" по дереву children (all = 0).
func (inv *Inventory) depth(group string) int {
if group == "all" {
return 0
}
best := -1
for name, g := range inv.Groups {
for _, child := range g.Children {
if child == group {
d := inv.depth(name) + 1
if best == -1 || d < best {
best = d
}
}
}
}
if best == -1 {
return 0 // группа без родителя — считаем что она на уровне all
}
return best
}
// GroupChain возвращает группы хоста в порядке применения group_vars:
// от самых общих (all, глубина 0) к самым специфичным; на одном уровне
// глубины — по алфавиту, чтобы результат был детерминирован.
func (inv *Inventory) GroupChain(hostName string) []string {
groups := inv.GroupsOf(hostName)
sort.Slice(groups, func(i, j int) bool {
di, dj := inv.depth(groups[i]), inv.depth(groups[j])
if di != dj {
return di < dj
}
return groups[i] < groups[j]
})
return groups
}
// GroupVars отдаёт переменные группы из самого инвентаря, смешанные
// с файлами group_vars/<name>.yml и group_vars/<name>/*.yml с диска.
func (inv *Inventory) GroupVars(name string) map[string]interface{} {
out := map[string]interface{}{}
if g, ok := inv.Groups[name]; ok {
for k, v := range g.Vars {
out[k] = v
}
}
for k, v := range inv.loadVarsFromDisk("group_vars", name) {
out[k] = v
}
return out
}
// HostVars — аналогично GroupVars, но для конкретного хоста
// (host_vars/<name>.yml или host_vars/<name>/*.yml).
func (inv *Inventory) HostVars(name string) map[string]interface{} {
out := map[string]interface{}{}
for _, g := range inv.Groups {
if h, ok := g.Hosts[name]; ok {
for k, v := range h.Vars {
out[k] = v
}
}
}
for k, v := range inv.loadVarsFromDisk("host_vars", name) {
out[k] = v
}
return out
}
// loadVarsFromDisk грузит либо <kind>/<name>.yml, либо все файлы
// в директории <kind>/<name>/*.yml по алфавиту, мерджа их по порядку.
func (inv *Inventory) loadVarsFromDisk(kind, name string) map[string]interface{} {
out := map[string]interface{}{}
singleFile := filepath.Join(inv.Dir, kind, name+".yml")
if vars, err := loadVarsFile(singleFile); err == nil {
for k, v := range vars {
out[k] = v
}
}
dir := filepath.Join(inv.Dir, kind, name)
entries, err := os.ReadDir(dir)
if err != nil {
return out
}
names := make([]string, 0, len(entries))
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".yml") {
names = append(names, e.Name())
}
}
sort.Strings(names)
for _, n := range names {
if vars, err := loadVarsFile(filepath.Join(dir, n)); err == nil {
for k, v := range vars {
out[k] = v
}
}
}
return out
}