ae948c7f04
A differently quoted substring inside a ${...} interpolation ended the
outer string early and truncated the argument list, dropping every name
after it. One shared walker now backs every source scan.
Lowercase the recovered prefix of an unresolved reference, and every
class name as it is added, so a mixed-case interpolated name still
widens.
Report fact records with no certname instead of dropping them, and
assert stripSource keeps newline positions rather than just length.
150 lines
3.5 KiB
Go
150 lines
3.5 KiB
Go
// Package impact maps a change in the Puppet control repo onto the hosts whose
|
|
// catalogs that change can alter.
|
|
package impact
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.unkin.net/unkin/puppet-catalog-tools/internal/pdbmux"
|
|
)
|
|
|
|
// FactNames are the facts every selector is evaluated against. They are
|
|
// fetched for the whole estate in one query.
|
|
var FactNames = []string{
|
|
"enc_role",
|
|
"enc_role_tier1",
|
|
"enc_role_tier2",
|
|
"enc_role_tier3",
|
|
"country",
|
|
"region",
|
|
"virtual",
|
|
"os",
|
|
}
|
|
|
|
// Node is the slice of a node's facts the hiera hierarchy keys off.
|
|
type Node struct {
|
|
Certname string
|
|
Role string
|
|
Tier1 string
|
|
Tier2 string
|
|
Tier3 string
|
|
Country string
|
|
Region string
|
|
Virtual string
|
|
OSName string
|
|
OSMajor string
|
|
OSMinor string
|
|
}
|
|
|
|
// Tiers returns the role tiers as a slice, truncated at the first empty one.
|
|
func (n Node) Tiers() []string {
|
|
tiers := make([]string, 0, 3)
|
|
for _, t := range []string{n.Tier1, n.Tier2, n.Tier3} {
|
|
if t == "" {
|
|
break
|
|
}
|
|
tiers = append(tiers, t)
|
|
}
|
|
return tiers
|
|
}
|
|
|
|
// FactSource supplies the estate-wide fact table.
|
|
type FactSource interface {
|
|
FactsByName(ctx context.Context, names []string) ([]pdbmux.FactRecord, error)
|
|
}
|
|
|
|
// LoadNodes fetches FactNames for every node and folds them into a sorted
|
|
// table. Nodes without an enc_role are kept: they still match country, os and
|
|
// virtual selectors. A record with no certname belongs to no host and cannot
|
|
// be kept, so it is returned as a warning rather than dropped in silence.
|
|
func LoadNodes(ctx context.Context, src FactSource) ([]Node, []string, error) {
|
|
records, err := src.FactsByName(ctx, FactNames)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
byName := map[string]*Node{}
|
|
anonymous := map[string]int{}
|
|
for _, r := range records {
|
|
if r.Certname == "" {
|
|
anonymous[r.Name]++
|
|
continue
|
|
}
|
|
n, ok := byName[r.Certname]
|
|
if !ok {
|
|
n = &Node{Certname: r.Certname}
|
|
byName[r.Certname] = n
|
|
}
|
|
applyFact(n, r.Name, r.Value)
|
|
}
|
|
|
|
nodes := make([]Node, 0, len(byName))
|
|
for _, n := range byName {
|
|
nodes = append(nodes, *n)
|
|
}
|
|
sort.Slice(nodes, func(i, j int) bool { return nodes[i].Certname < nodes[j].Certname })
|
|
if len(nodes) == 0 {
|
|
return nil, anonymousWarnings(anonymous), fmt.Errorf("pdbmux returned no nodes")
|
|
}
|
|
return nodes, anonymousWarnings(anonymous), nil
|
|
}
|
|
|
|
// anonymousWarnings renders the dropped-record tally one line per fact.
|
|
func anonymousWarnings(counts map[string]int) []string {
|
|
names := make([]string, 0, len(counts))
|
|
for name := range counts {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
out := make([]string, 0, len(names))
|
|
for _, name := range names {
|
|
out = append(out, fmt.Sprintf("fact %q: %d record(s) had no certname and were dropped", name, counts[name]))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func applyFact(n *Node, name string, value any) {
|
|
switch name {
|
|
case "enc_role":
|
|
n.Role = str(value)
|
|
case "enc_role_tier1":
|
|
n.Tier1 = str(value)
|
|
case "enc_role_tier2":
|
|
n.Tier2 = str(value)
|
|
case "enc_role_tier3":
|
|
n.Tier3 = str(value)
|
|
case "country":
|
|
n.Country = str(value)
|
|
case "region":
|
|
n.Region = str(value)
|
|
case "virtual":
|
|
n.Virtual = str(value)
|
|
case "os":
|
|
os, ok := value.(map[string]any)
|
|
if !ok {
|
|
return
|
|
}
|
|
n.OSName = str(os["name"])
|
|
release, ok := os["release"].(map[string]any)
|
|
if !ok {
|
|
return
|
|
}
|
|
n.OSMajor = str(release["major"])
|
|
n.OSMinor = str(release["minor"])
|
|
}
|
|
}
|
|
|
|
func str(v any) string {
|
|
switch t := v.(type) {
|
|
case string:
|
|
return t
|
|
case nil:
|
|
return ""
|
|
default:
|
|
return strings.TrimSpace(fmt.Sprint(t))
|
|
}
|
|
}
|