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
103 lines
2.3 KiB
Go
103 lines
2.3 KiB
Go
package impact
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
// RenderPlain writes one hostname per line, ready to pipe.
|
|
func RenderPlain(w io.Writer, r *Result) error {
|
|
for _, h := range r.Hosts {
|
|
if _, err := fmt.Fprintln(w, h.Certname); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RenderJSON writes the structured report.
|
|
func RenderJSON(w io.Writer, r *Result) error {
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", " ")
|
|
if r.Hosts == nil {
|
|
r.Hosts = []Host{}
|
|
}
|
|
if r.ChangedFiles == nil {
|
|
r.ChangedFiles = []string{}
|
|
}
|
|
if r.UnmappedFiles == nil {
|
|
r.UnmappedFiles = []string{}
|
|
}
|
|
return enc.Encode(r)
|
|
}
|
|
|
|
// RenderExplain writes the human-readable report: what each file mapped to,
|
|
// and for each host which changed files reach it.
|
|
func RenderExplain(w io.Writer, r *Result) error {
|
|
p := func(format string, args ...any) error {
|
|
_, err := fmt.Fprintf(w, format+"\n", args...)
|
|
return err
|
|
}
|
|
|
|
if err := p("%s...%s", r.Base, r.Branch); err != nil {
|
|
return err
|
|
}
|
|
if err := p("%d changed file(s), %d of %d host(s) affected", r.Summary.Files, r.Summary.Hosts, r.TotalHosts); err != nil {
|
|
return err
|
|
}
|
|
if r.Summary.AllHostsTriggered {
|
|
if err := p("the whole estate is affected"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if r.Truncated {
|
|
if err := p("host list truncated by --limit"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := section(w, "unmapped files (widened to all hosts)", r.UnmappedFiles); err != nil {
|
|
return err
|
|
}
|
|
if err := section(w, "ignored files (outside the Puppet modulepath)", r.IgnoredFiles); err != nil {
|
|
return err
|
|
}
|
|
if err := section(w, "orphan classes (nothing declares them and no hiera_include lists them)", r.OrphanClasses); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(r.Hosts) == 0 {
|
|
return p("\nno hosts affected")
|
|
}
|
|
if err := p("\nhosts:"); err != nil {
|
|
return err
|
|
}
|
|
for _, h := range r.Hosts {
|
|
if err := p(" %s", h.Certname); err != nil {
|
|
return err
|
|
}
|
|
for _, reason := range h.Reasons {
|
|
if err := p(" %s [%s] %s", reason.File, reason.Rule, reason.Detail); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func section(w io.Writer, title string, items []string) error {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
if _, err := fmt.Fprintf(w, "\n%s:\n", title); err != nil {
|
|
return err
|
|
}
|
|
for _, item := range items {
|
|
if _, err := fmt.Fprintf(w, " %s\n", item); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|