Files
unkin-agent 04ec9e6d41
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
carry dropped fact records through the failure path and count them
Report the accumulated warnings on stderr when the estate load fails, and fold
the dropped-record tally into the error. Count records rather than warning
messages in --explain, by carrying the count on the Warn hook.
2026-09-12 20:27:55 +10:00

140 lines
3.5 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{}
}
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
}