04ec9e6d41
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.
236 lines
8.0 KiB
Go
236 lines
8.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const estateBody = `[
|
|
{"certname":"web1.example.net","name":"enc_role","value":"roles::apps::media::jellyfin"},
|
|
{"certname":"web1.example.net","name":"enc_role_tier1","value":"apps"},
|
|
{"certname":"web1.example.net","name":"enc_role_tier2","value":"media"},
|
|
{"certname":"web1.example.net","name":"enc_role_tier3","value":"jellyfin"},
|
|
{"certname":"web1.example.net","name":"country","value":"au"},
|
|
{"certname":"web1.example.net","name":"region","value":"syd1"},
|
|
{"certname":"web1.example.net","name":"virtual","value":"lxc"},
|
|
{"certname":"web1.example.net","name":"os","value":{"name":"AlmaLinux","release":{"major":"9","minor":"7"}}},
|
|
{"certname":"k8s1.example.net","name":"enc_role","value":"roles::infra::k8s::node"},
|
|
{"certname":"k8s1.example.net","name":"enc_role_tier1","value":"infra"},
|
|
{"certname":"k8s1.example.net","name":"enc_role_tier2","value":"k8s"},
|
|
{"certname":"k8s1.example.net","name":"enc_role_tier3","value":"node"},
|
|
{"certname":"k8s1.example.net","name":"country","value":"au"},
|
|
{"certname":"k8s1.example.net","name":"region","value":"syd1"},
|
|
{"certname":"k8s1.example.net","name":"virtual","value":"physical"},
|
|
{"certname":"k8s1.example.net","name":"os","value":{"name":"AlmaLinux","release":{"major":"9","minor":"8"}}}
|
|
]`
|
|
|
|
func fakePuppetDB(t *testing.T) string {
|
|
t.Helper()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(estateBody))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv.URL
|
|
}
|
|
|
|
func TestRunWithExplicitFileList(t *testing.T) {
|
|
var out bytes.Buffer
|
|
opts := &options{
|
|
repo: "../../internal/impact/testdata/repo",
|
|
base: "origin/develop",
|
|
files: []string{"modules/jellyfin/manifests/install.pp"},
|
|
puppetdb: fakePuppetDB(t),
|
|
timeout: 5 * time.Second,
|
|
}
|
|
if err := run(context.Background(), &out, io.Discard, opts); err != nil {
|
|
t.Fatalf("run: %v", err)
|
|
}
|
|
if got, want := out.String(), "web1.example.net\n"; got != want {
|
|
t.Errorf("output = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRunExplain(t *testing.T) {
|
|
var out bytes.Buffer
|
|
opts := &options{
|
|
repo: "../../internal/impact/testdata/repo",
|
|
base: "origin/develop",
|
|
files: []string{"hieradata/common.yaml"},
|
|
puppetdb: fakePuppetDB(t),
|
|
explain: true,
|
|
timeout: 5 * time.Second,
|
|
}
|
|
if err := run(context.Background(), &out, io.Discard, opts); err != nil {
|
|
t.Fatalf("run: %v", err)
|
|
}
|
|
if !strings.Contains(out.String(), "the whole estate is affected") {
|
|
t.Errorf("explain output = %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestRunRejectsConflictingOutputFlags(t *testing.T) {
|
|
opts := &options{jsonOut: true, explain: true}
|
|
if err := run(context.Background(), &bytes.Buffer{}, io.Discard, opts); err == nil {
|
|
t.Fatal("expected an error for --json with --explain")
|
|
}
|
|
}
|
|
|
|
func TestRunRejectsNegativeLimit(t *testing.T) {
|
|
opts := &options{limit: -1}
|
|
if err := run(context.Background(), &bytes.Buffer{}, io.Discard, opts); err == nil {
|
|
t.Fatal("expected an error for a negative --limit")
|
|
}
|
|
}
|
|
|
|
// undecodableEstateBody strips the value from every one of web1's records, so
|
|
// each decodes to nothing and the host leaves the estate table with it.
|
|
const undecodableEstateBody = `[
|
|
{"certname":"web1.example.net","name":"enc_role"},
|
|
{"certname":"web1.example.net","name":"country"},
|
|
{"certname":"web1.example.net","name":"os"},
|
|
{"certname":"k8s1.example.net","name":"enc_role","value":"roles::infra::k8s::node"},
|
|
{"certname":"k8s1.example.net","name":"country","value":"au"},
|
|
{"certname":"k8s1.example.net","name":"os","value":{"name":"AlmaLinux","release":{"major":"9","minor":"8"}}}
|
|
]`
|
|
|
|
func fakePuppetDBBody(t *testing.T, body string) string {
|
|
t.Helper()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(body))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv.URL
|
|
}
|
|
|
|
func droppedFactOptions(t *testing.T) *options {
|
|
t.Helper()
|
|
return &options{
|
|
repo: "../../internal/impact/testdata/repo",
|
|
base: "origin/develop",
|
|
files: []string{"modules/jellyfin/manifests/install.pp"},
|
|
puppetdb: fakePuppetDBBody(t, undecodableEstateBody),
|
|
timeout: 5 * time.Second,
|
|
}
|
|
}
|
|
|
|
func TestRunReportsDroppedFactRecordsOnStderr(t *testing.T) {
|
|
var out, errOut bytes.Buffer
|
|
if err := run(context.Background(), &out, &errOut, droppedFactOptions(t)); err != nil {
|
|
t.Fatalf("run: %v", err)
|
|
}
|
|
if out.String() != "" {
|
|
t.Fatalf("web1 should have dropped out of the estate table, output = %q", out.String())
|
|
}
|
|
for _, want := range []string{"skipped 3 undecodable fact(s)", "the estate table may be missing hosts"} {
|
|
if !strings.Contains(errOut.String(), want) {
|
|
t.Errorf("stderr = %q, want it to mention %q", errOut.String(), want)
|
|
}
|
|
}
|
|
if got := strings.Count(errOut.String(), "\n"); got != 1 {
|
|
t.Errorf("stderr has %d warning line(s), want 1: %q", got, errOut.String())
|
|
}
|
|
}
|
|
|
|
func TestRunReportsDroppedFactRecordsInJSON(t *testing.T) {
|
|
var out bytes.Buffer
|
|
opts := droppedFactOptions(t)
|
|
opts.jsonOut = true
|
|
if err := run(context.Background(), &out, io.Discard, opts); err != nil {
|
|
t.Fatalf("run: %v", err)
|
|
}
|
|
|
|
var doc struct {
|
|
NodeWarnings []string `json:"node_warnings"`
|
|
Summary struct {
|
|
ResolutionIncomplete bool `json:"resolution_incomplete"`
|
|
DroppedFactRecords int `json:"dropped_fact_records"`
|
|
} `json:"summary"`
|
|
}
|
|
if err := json.Unmarshal(out.Bytes(), &doc); err != nil {
|
|
t.Fatalf("decoding --json output: %v", err)
|
|
}
|
|
if len(doc.NodeWarnings) != 1 {
|
|
t.Errorf("node_warnings = %v, want the one batched warning", doc.NodeWarnings)
|
|
}
|
|
if !doc.Summary.ResolutionIncomplete {
|
|
t.Error("resolution_incomplete is false after records were dropped")
|
|
}
|
|
if doc.Summary.DroppedFactRecords != 3 {
|
|
t.Errorf("summary.dropped_fact_records = %d, want 3", doc.Summary.DroppedFactRecords)
|
|
}
|
|
}
|
|
|
|
func TestRunReportsDroppedFactRecordsInExplain(t *testing.T) {
|
|
var out bytes.Buffer
|
|
opts := droppedFactOptions(t)
|
|
opts.explain = true
|
|
if err := run(context.Background(), &out, io.Discard, opts); err != nil {
|
|
t.Fatalf("run: %v", err)
|
|
}
|
|
// web1 dropped three records in one warning, so the headline must count
|
|
// records, not messages.
|
|
for _, want := range []string{
|
|
"fact records dropped",
|
|
"3 fact record(s) dropped",
|
|
"skipped 3 undecodable fact(s)",
|
|
} {
|
|
if !strings.Contains(out.String(), want) {
|
|
t.Errorf("explain output = %q, want it to mention %q", out.String(), want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// allUndecodableEstateBody strips the value from every record of every host,
|
|
// so the estate table comes back empty and the load fails.
|
|
const allUndecodableEstateBody = `[
|
|
{"certname":"web1.example.net","name":"enc_role"},
|
|
{"certname":"web1.example.net","name":"country"},
|
|
{"certname":"k8s1.example.net","name":"enc_role"},
|
|
{"certname":"k8s1.example.net","name":"country"}
|
|
]`
|
|
|
|
func TestRunReportsDroppedFactRecordsWhenTheLoadFails(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
jsonOut bool
|
|
explain bool
|
|
}{
|
|
{name: "plain"},
|
|
{name: "json", jsonOut: true},
|
|
{name: "explain", explain: true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var out, errOut bytes.Buffer
|
|
opts := &options{
|
|
repo: "../../internal/impact/testdata/repo",
|
|
base: "origin/develop",
|
|
files: []string{"modules/jellyfin/manifests/install.pp"},
|
|
puppetdb: fakePuppetDBBody(t, allUndecodableEstateBody),
|
|
jsonOut: tc.jsonOut,
|
|
explain: tc.explain,
|
|
timeout: 5 * time.Second,
|
|
}
|
|
|
|
err := run(context.Background(), &out, &errOut, opts)
|
|
if err == nil {
|
|
t.Fatal("an empty estate table must fail the run")
|
|
}
|
|
if !strings.Contains(err.Error(), "4 fact record(s) dropped") {
|
|
t.Errorf("error = %q, want it to account for the 4 dropped records", err)
|
|
}
|
|
if !strings.Contains(errOut.String(), "skipped 4 undecodable fact(s)") {
|
|
t.Errorf("stderr = %q, want the dropped records reported", errOut.String())
|
|
}
|
|
})
|
|
}
|
|
}
|