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{} } if r.UnresolvedRefs == nil { r.UnresolvedRefs = []UnresolvedRef{} } if r.NodeWarnings == nil { r.NodeWarnings = []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 r.Summary.ResolutionIncomplete { if err := p("resolution incomplete: %d class reference(s) unresolved, %d fact record(s) dropped", len(r.UnresolvedRefs), r.Summary.DroppedFactRecords); err != nil { return err } } if err := section(w, "fact records dropped (the estate table may be missing hosts)", r.NodeWarnings); 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, "changed files that reach no host (the role they belong to has no members)", r.EmptyFiles); err != nil { return err } if err := section(w, "unresolved class references (the host list may be incomplete)", UnresolvedLines(r.UnresolvedRefs)); 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 } // UnresolvedLines renders unresolved references one per line for the report // and for the stderr warnings. func UnresolvedLines(refs []UnresolvedRef) []string { out := make([]string, 0, len(refs)) for _, u := range refs { line := fmt.Sprintf("%s: %s %s", u.File, u.Form, u.Expr) if u.Prefix != "" { line += fmt.Sprintf(" (names a class under %s)", u.Prefix) } out = append(out, line) } return out }