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
124 lines
2.9 KiB
Go
124 lines
2.9 KiB
Go
package impact
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Selector is a conjunction of fact constraints. Every field left at its zero
|
|
// value is unconstrained, so an empty Selector with All unset matches nothing
|
|
// and is treated as a programming error by the callers that build them.
|
|
type Selector struct {
|
|
// All matches the whole estate (hieradata/common.yaml, repo-global files).
|
|
All bool
|
|
// Certname pins one host (hieradata/nodes/<certname>.yaml).
|
|
Certname string
|
|
// RoleTiers is a prefix match on enc_role_tier1..3, so a two-tier
|
|
// hieradata file covers every role nested under it.
|
|
RoleTiers []string
|
|
// RoleExact matches enc_role verbatim, which is how a role class maps to
|
|
// hosts.
|
|
RoleExact string
|
|
Country string
|
|
Region string
|
|
Virtual string
|
|
OSName string
|
|
OSMajor string
|
|
OSMinor string
|
|
}
|
|
|
|
// Match reports whether the node satisfies every constraint set on s.
|
|
func (s Selector) Match(n Node) bool {
|
|
if s.All {
|
|
return true
|
|
}
|
|
if s.Certname != "" && s.Certname != n.Certname {
|
|
return false
|
|
}
|
|
if s.RoleExact != "" && s.RoleExact != n.Role {
|
|
return false
|
|
}
|
|
if s.Country != "" && s.Country != n.Country {
|
|
return false
|
|
}
|
|
if s.Region != "" && s.Region != n.Region {
|
|
return false
|
|
}
|
|
if s.Virtual != "" && s.Virtual != n.Virtual {
|
|
return false
|
|
}
|
|
if s.OSName != "" && !strings.EqualFold(s.OSName, n.OSName) {
|
|
return false
|
|
}
|
|
if s.OSMajor != "" && s.OSMajor != n.OSMajor {
|
|
return false
|
|
}
|
|
if s.OSMinor != "" && s.OSMinor != n.OSMinor {
|
|
return false
|
|
}
|
|
if len(s.RoleTiers) > 0 {
|
|
tiers := n.Tiers()
|
|
if len(tiers) < len(s.RoleTiers) {
|
|
return false
|
|
}
|
|
for i, want := range s.RoleTiers {
|
|
if tiers[i] != want {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Select returns the certnames of every matching node, sorted.
|
|
func (s Selector) Select(nodes []Node) []string {
|
|
var out []string
|
|
for _, n := range nodes {
|
|
if s.Match(n) {
|
|
out = append(out, n.Certname)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// String renders the selector the way --explain prints it.
|
|
func (s Selector) String() string {
|
|
if s.All {
|
|
return "all hosts"
|
|
}
|
|
var parts []string
|
|
add := func(k, v string) {
|
|
if v != "" {
|
|
parts = append(parts, k+"="+v)
|
|
}
|
|
}
|
|
add("certname", s.Certname)
|
|
add("enc_role", s.RoleExact)
|
|
if len(s.RoleTiers) > 0 {
|
|
parts = append(parts, "role-prefix="+strings.Join(s.RoleTiers, "::"))
|
|
}
|
|
add("country", s.Country)
|
|
add("region", s.Region)
|
|
add("virtual", s.Virtual)
|
|
add("os.name", s.OSName)
|
|
add("os.release.major", s.OSMajor)
|
|
add("os.release.minor", s.OSMinor)
|
|
if len(parts) == 0 {
|
|
return "nothing"
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
// target is one resolved "this file reaches these hosts, for this reason".
|
|
type target struct {
|
|
sel Selector
|
|
rule string
|
|
detail string
|
|
}
|
|
|
|
func allTarget(rule, detail string) target {
|
|
return target{sel: Selector{All: true}, rule: rule, detail: detail}
|
|
}
|
|
|
|
func fmtDetail(format string, args ...any) string { return fmt.Sprintf(format, args...) }
|