Files
puppet-catalog-tools/internal/impact/node.go
T
unkin-agent ae948c7f04
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
track the opening quote and scan interpolations as code
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.
2026-09-12 17:54:03 +10:00

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))
}
}