c4d3ed24a9
- walk the classes holding an unresolved reference when tracing any class it could have named, so a dropped edge cannot lose hosts - list unresolved references in --explain, in unresolved_references in --json and on stderr, and flag summary.resolution_incomplete - name an ordering reference as such in the --explain chain
167 lines
5.8 KiB
Go
167 lines
5.8 KiB
Go
package impact
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// tempRepo writes a throwaway control repo from a path -> content map.
|
|
func tempRepo(t *testing.T, files map[string]string) string {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
for rel, content := range files {
|
|
full := filepath.Join(root, filepath.FromSlash(rel))
|
|
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(full, []byte(content), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return root
|
|
}
|
|
|
|
func buildIndex(t *testing.T, files map[string]string) *Index {
|
|
t.Helper()
|
|
idx, err := BuildIndex(tempRepo(t, files))
|
|
if err != nil {
|
|
t.Fatalf("BuildIndex: %v", err)
|
|
}
|
|
return idx
|
|
}
|
|
|
|
func TestIndexRecordsUnresolvedReferenceAgainstItsFile(t *testing.T) {
|
|
idx := testIndex(t)
|
|
got := idx.UnresolvedIn("site/profiles/manifests/dynamic.pp")
|
|
want := []UnresolvedRef{{
|
|
File: "site/profiles/manifests/dynamic.pp",
|
|
Form: "include",
|
|
Expr: `"profiles::dyn_${facts['role']}"`,
|
|
Prefix: "profiles::dyn_",
|
|
}}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("UnresolvedIn = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestResolveWidensThroughAnUnresolvedPrefix(t *testing.T) {
|
|
// profiles::dynamic includes "profiles::dyn_${...}". Any class under that
|
|
// prefix must reach whatever profiles::dynamic reaches.
|
|
if got, want := resolvedHosts(t, "profiles::dyn_web"), []string{"k8s1", "k8s2", "web1"}; !reflect.DeepEqual(got, want) {
|
|
t.Errorf("profiles::dyn_web = %v, want %v", got, want)
|
|
}
|
|
// A class outside the prefix is not dragged in by it.
|
|
res := newClassResolver(testIndex(t), fixtureNodes()).resolve("profiles::unrelated_thing")
|
|
if !res.orphan {
|
|
t.Errorf("an unrelated class was widened to %v", res.hosts)
|
|
}
|
|
}
|
|
|
|
func TestResolveWidensThroughAnUnresolvedVariable(t *testing.T) {
|
|
// With no literal prefix to match on, every class is a candidate, so the
|
|
// holding class's hosts are included rather than dropped.
|
|
idx := buildIndex(t, map[string]string{
|
|
"site/roles/manifests/base.pp": "class roles::base {\n include profiles::base\n}\n",
|
|
"site/profiles/manifests/base.pp": "class profiles::base {\n include profiles::wild\n}\n",
|
|
"site/profiles/manifests/wild.pp": "class profiles::wild {\n $c = lookup('profile')\n include $c\n}\n",
|
|
"site/profiles/manifests/standalone.pp": "class profiles::standalone {\n}\n",
|
|
})
|
|
res := newClassResolver(idx, fixtureNodes()).resolve("profiles::standalone")
|
|
if got, want := uniqueSorted(hostNames(res)), []string{"bare1"}; !reflect.DeepEqual(got, want) {
|
|
t.Errorf("hosts = %v, want %v", got, want)
|
|
}
|
|
if len(res.unresolved) != 1 || res.unresolved[0].File != "site/profiles/manifests/wild.pp" {
|
|
t.Errorf("resolution did not report the unresolved reference: %#v", res.unresolved)
|
|
}
|
|
}
|
|
|
|
func hostNames(res *resolution) []string {
|
|
out := make([]string, 0, len(res.hosts))
|
|
for certname := range res.hosts {
|
|
out = append(out, certname)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestAnalyzeReportsUnresolvedReferencesInAChangedFile(t *testing.T) {
|
|
r := analyze(t, "site/profiles/manifests/dynamic.pp")
|
|
if !r.Summary.ResolutionIncomplete {
|
|
t.Error("summary did not flag the incomplete resolution")
|
|
}
|
|
if len(r.UnresolvedRefs) != 1 || r.UnresolvedRefs[0].Prefix != "profiles::dyn_" {
|
|
t.Fatalf("UnresolvedRefs = %#v", r.UnresolvedRefs)
|
|
}
|
|
|
|
var explain bytes.Buffer
|
|
if err := RenderExplain(&explain, r); err != nil {
|
|
t.Fatalf("RenderExplain: %v", err)
|
|
}
|
|
for _, want := range []string{"resolution incomplete", "unresolved class references",
|
|
"site/profiles/manifests/dynamic.pp", "profiles::dyn_"} {
|
|
if !strings.Contains(explain.String(), want) {
|
|
t.Errorf("explain output missing %q:\n%s", want, explain.String())
|
|
}
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
if err := RenderJSON(&out, r); err != nil {
|
|
t.Fatalf("RenderJSON: %v", err)
|
|
}
|
|
var doc struct {
|
|
UnresolvedRefs []UnresolvedRef `json:"unresolved_references"`
|
|
Summary struct {
|
|
ResolutionIncomplete bool `json:"resolution_incomplete"`
|
|
} `json:"summary"`
|
|
}
|
|
if err := json.Unmarshal(out.Bytes(), &doc); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if !doc.Summary.ResolutionIncomplete || len(doc.UnresolvedRefs) != 1 {
|
|
t.Errorf("json = %s", out.String())
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeReportsNoUnresolvedReferencesForACleanFile(t *testing.T) {
|
|
r := analyze(t, "site/profiles/manifests/leaf.pp")
|
|
if r.Summary.ResolutionIncomplete || len(r.UnresolvedRefs) != 0 {
|
|
t.Errorf("clean file reported %#v", r.UnresolvedRefs)
|
|
}
|
|
}
|
|
|
|
func TestExplainCallsAnOrderingReferenceWhatItIs(t *testing.T) {
|
|
idx := buildIndex(t, map[string]string{
|
|
"site/roles/manifests/base.pp": "class roles::base {\n include profiles::base\n}\n",
|
|
"site/profiles/manifests/base.pp": "class profiles::base {\n service { 's': require => Class['profiles::ordered'] }\n}\n",
|
|
"site/profiles/manifests/ordered.pp": "class profiles::ordered {\n}\n",
|
|
})
|
|
res := newClassResolver(idx, fixtureNodes()).resolve("profiles::ordered")
|
|
hits := res.hosts["bare1"]
|
|
if len(hits) == 0 {
|
|
t.Fatalf("profiles::ordered reached no host: %#v", res.hosts)
|
|
}
|
|
if !strings.Contains(hits[0].detail, "not a declaration") {
|
|
t.Errorf("detail = %q, want it to mark the ordering reference", hits[0].detail)
|
|
}
|
|
}
|
|
|
|
func TestExplainLeavesADeclarationUnannotated(t *testing.T) {
|
|
idx := buildIndex(t, map[string]string{
|
|
"site/roles/manifests/base.pp": "class roles::base {\n include profiles::base\n}\n",
|
|
"site/profiles/manifests/base.pp": "class profiles::base {\n include profiles::ordered\n}\n",
|
|
"site/profiles/manifests/ordered.pp": "class profiles::ordered {\n}\n",
|
|
})
|
|
res := newClassResolver(idx, fixtureNodes()).resolve("profiles::ordered")
|
|
hits := res.hosts["bare1"]
|
|
if len(hits) == 0 {
|
|
t.Fatalf("profiles::ordered reached no host: %#v", res.hosts)
|
|
}
|
|
if strings.Contains(hits[0].detail, "not a declaration") {
|
|
t.Errorf("detail = %q, an include is a declaration", hits[0].detail)
|
|
}
|
|
}
|