8093933500
Work out which hosts a puppet-prod change can affect, so a catalog test can
fan out over exactly those hosts instead of the whole estate.
- add cmd/catalog-impact and internal/impact
- map hieradata paths onto fact selectors from the hiera hierarchy
- trace manifest classes back to role classes through include, contain,
class {} and hiera_include, to a fixed point
- fetch the estate fact table from pdbmux in one query via FactsByName
- widen to all hosts, loudly, for any path that matches no rule
- ship the binary and its completions in the RPM
133 lines
2.8 KiB
Go
133 lines
2.8 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.
|
|
func LoadNodes(ctx context.Context, src FactSource) ([]Node, error) {
|
|
records, err := src.FactsByName(ctx, FactNames)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
byName := map[string]*Node{}
|
|
for _, r := range records {
|
|
if r.Certname == "" {
|
|
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, fmt.Errorf("pdbmux returned no nodes")
|
|
}
|
|
return nodes, nil
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|