c4d3ed24a9
- walk the classes holding an unresolved reference when tracing any class it could have named, so a dropped edge cannot lose hosts - list unresolved references in --explain, in unresolved_references in --json and on stderr, and flag summary.resolution_incomplete - name an ordering reference as such in the --explain chain
158 lines
4.5 KiB
Go
158 lines
4.5 KiB
Go
package impact
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// hit is one reason a host is affected, before it is attached to a file.
|
|
type hit struct {
|
|
rule string
|
|
detail string
|
|
}
|
|
|
|
// classResolver walks the include graph backwards from a changed class to the
|
|
// hosts that end up with it.
|
|
type classResolver struct {
|
|
idx *Index
|
|
nodes []Node
|
|
cache map[string]*resolution
|
|
}
|
|
|
|
type resolution struct {
|
|
hosts map[string][]hit
|
|
orphan bool
|
|
// unresolved are the references whose incompleteness widened this walk.
|
|
unresolved []UnresolvedRef
|
|
}
|
|
|
|
func newClassResolver(idx *Index, nodes []Node) *classResolver {
|
|
return &classResolver{idx: idx, nodes: nodes, cache: map[string]*resolution{}}
|
|
}
|
|
|
|
// resolve returns the hosts a class reaches. The walk runs to a fixed point
|
|
// over both routes a class can be applied by — a manifest declaring it, and a
|
|
// hieradata file listing it under hiera_include — and stops at role classes,
|
|
// which the ENC assigns directly.
|
|
func (r *classResolver) resolve(class string) *resolution {
|
|
if cached, ok := r.cache[class]; ok {
|
|
return cached
|
|
}
|
|
res := &resolution{hosts: map[string][]hit{}}
|
|
r.cache[class] = res
|
|
|
|
// via records how each class was reached, so --explain can print the chain.
|
|
via := map[string]string{class: class}
|
|
seen := map[string]bool{class: true}
|
|
queue := []string{class}
|
|
reached := false
|
|
|
|
for len(queue) > 0 {
|
|
current := queue[0]
|
|
queue = queue[1:]
|
|
|
|
if strings.HasPrefix(current, "roles::") {
|
|
sel := Selector{RoleExact: current}
|
|
detail := fmtDetail("role %s carries %s (%s)", current, class, via[current])
|
|
for _, certname := range sel.Select(r.nodes) {
|
|
res.hosts[certname] = append(res.hosts[certname], hit{rule: RuleRoleClass, detail: detail})
|
|
}
|
|
reached = true
|
|
}
|
|
|
|
if hosts, detail, ok := r.viaHiera(current); ok {
|
|
reached = true
|
|
for _, certname := range hosts {
|
|
res.hosts[certname] = append(res.hosts[certname],
|
|
hit{rule: RuleHieraInclude, detail: detail + " (" + via[current] + ")"})
|
|
}
|
|
}
|
|
|
|
for _, parent := range r.idx.IncludedBy(current) {
|
|
if seen[parent] {
|
|
continue
|
|
}
|
|
seen[parent] = true
|
|
via[parent] = via[current] + " <- " + parent + r.idx.EdgeNote(current, parent)
|
|
queue = append(queue, parent)
|
|
}
|
|
|
|
// A reference the parser could not name might have been this class, so
|
|
// the classes holding it are walked too rather than dropped.
|
|
for _, u := range r.idx.UnresolvedIncluders(current) {
|
|
for _, parent := range u.Classes {
|
|
if seen[parent] {
|
|
continue
|
|
}
|
|
seen[parent] = true
|
|
via[parent] = via[current] + " <- " + parent +
|
|
fmtDetail(" (unresolved %s %s in %s)", u.Ref.Form, u.Ref.Expr, u.Ref.File)
|
|
queue = append(queue, parent)
|
|
}
|
|
res.unresolved = append(res.unresolved, u.Ref)
|
|
}
|
|
}
|
|
|
|
res.orphan = !reached
|
|
res.unresolved = dedupeUnresolved(res.unresolved)
|
|
return res
|
|
}
|
|
|
|
// viaHiera applies a class that hieradata pulls in with hiera_include. The
|
|
// hierarchy position of the listing file decides which hosts get it, and
|
|
// hiera_exclude entries are subtracted because profiles::base declares
|
|
// ($hiera_include - $hiera_exclude).
|
|
func (r *classResolver) viaHiera(class string) ([]string, string, bool) {
|
|
includes := r.idx.HieraInclude(class)
|
|
if len(includes) == 0 {
|
|
return nil, "", false
|
|
}
|
|
|
|
included := map[string]bool{}
|
|
var sources []string
|
|
for _, file := range includes {
|
|
sel := hieraFileSelector(file)
|
|
for _, certname := range sel.Select(r.nodes) {
|
|
included[certname] = true
|
|
}
|
|
sources = append(sources, fmtDetail("%s [%s]", file, sel))
|
|
}
|
|
|
|
excludes := r.idx.HieraExclude(class)
|
|
var excluded []string
|
|
for _, file := range excludes {
|
|
sel := hieraFileSelector(file)
|
|
for _, certname := range sel.Select(r.nodes) {
|
|
if included[certname] {
|
|
delete(included, certname)
|
|
excluded = append(excluded, certname)
|
|
}
|
|
}
|
|
}
|
|
|
|
hosts := make([]string, 0, len(included))
|
|
for certname := range included {
|
|
hosts = append(hosts, certname)
|
|
}
|
|
sort.Strings(hosts)
|
|
|
|
detail := fmtDetail("hiera_include %s in %s", class, strings.Join(sources, ", "))
|
|
if len(excluded) > 0 {
|
|
detail += fmtDetail("; hiera_exclude in %s drops %d host(s)",
|
|
strings.Join(excludes, ", "), len(uniqueSorted(excluded)))
|
|
}
|
|
return hosts, detail, true
|
|
}
|
|
|
|
// hieraFileSelector maps a hieradata file back onto its hierarchy position. An
|
|
// unrecognised path widens to the whole estate rather than silently selecting
|
|
// nothing.
|
|
func hieraFileSelector(file string) Selector {
|
|
rel := strings.TrimPrefix(file, "hieradata/")
|
|
sel, _, ok := hieradataSelector(rel)
|
|
if !ok {
|
|
return Selector{All: true}
|
|
}
|
|
return sel
|
|
}
|