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 }