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/.yml и group_vars//*.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/.yml или host_vars//*.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 грузит либо /.yml, либо все файлы // в директории //*.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 }