a5f27348ae
Move FactsByName onto the renamed puppetdb client and share the facts query and record decoding with the single-node fetch.
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/puppetdb"
|
|
)
|
|
|
|
// 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) ([]puppetdb.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("puppetdb 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))
|
|
}
|
|
}
|