ae948c7f04
A differently quoted substring inside a ${...} interpolation ended the
outer string early and truncated the argument list, dropping every name
after it. One shared walker now backs every source scan.
Lowercase the recovered prefix of an unresolved reference, and every
class name as it is added, so a mixed-case interpolated name still
widens.
Report fact records with no certname instead of dropping them, and
assert stripSource keeps newline positions rather than just length.
140 lines
3.5 KiB
Go
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) could not be resolved statically",
|
|
len(r.UnresolvedRefs)); 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
|
|
}
|