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