A push leaves out a module whose settings do not compose and sends the rest, which is right, but only plan said so: nfs-server ran nowhere for days while the operator believed it ran. The self-check now raises needs-operator for a setting nobody gave, naming the setting and the command, and left-out for any other cause; both warnings, cleared once the module composes or is unassigned. status and node show list the same modules as assigned, not applied.
119 lines
4.6 KiB
Go
119 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// A provider that keeps failing a consumer is a problem `status` names (novox/hq ADR 0224), kept as
|
|
// the condition store's first kind (to-be 45 §2). On 2026-10-05 the identity provider refused every
|
|
// consumer for a day and status called the mesh well (04-ISSUES/179): this is that day, told to the
|
|
// controller the way the provider now tells it.
|
|
func TestAProviderFailingAConsumerBreaksAllWellUntilItRecovers(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
register(t, open, catalogue.Manifest{Module: "idp", Version: "1",
|
|
Receives: map[string]string{"oidc-client": "/var/lib/mesh/idp/mesh.json"}})
|
|
if _, err := assign(ctx, open, "anchor", "idp"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
kept := standings{keeper: func() *conditions.Keeper { return conditionsFrom }}
|
|
since := time.Now().Add(-23 * time.Hour)
|
|
failing := link.Standing{Module: "idp", Failing: true, Provider: "oidc-client", ProviderNode: "anchor",
|
|
Consumer: "mesh_laptop_dashboard", Node: "laptop", Class: "credentials-rejected",
|
|
Error: `Keycloak token request failed: 401 {"error":"invalid_grant"}`, Since: since, Attempts: 31000}
|
|
if _, err := kept.Stood(ctx, failing); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
asked, err := theThreeQuestions(ctx, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if asked.well() {
|
|
t.Fatal("a mesh whose identity provider fails a consumer reads as well")
|
|
}
|
|
said := printed(t, func() error { return printStatus(asked) })
|
|
for _, want := range []string{"1 open condition(s)", "provider.idp.anchor.mesh_laptop_dashboard.failing",
|
|
"idp on anchor keeps failing mesh_laptop_dashboard on laptop", "credentials-rejected", "31000 attempt(s)"} {
|
|
if !strings.Contains(said, want) {
|
|
t.Fatalf("status does not say %q:\n%s", want, said)
|
|
}
|
|
}
|
|
if strings.Contains(said, "all doing what they were told") {
|
|
t.Fatalf("status said all well beside a failing provider:\n%s", said)
|
|
}
|
|
if !strings.HasPrefix(said, "1 open condition(s)") {
|
|
t.Fatalf("status does not lead with what is open:\n%s", said)
|
|
}
|
|
body, err := statusAsJSON(asked)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var doc struct {
|
|
Conditions []conditions.Condition `json:"conditions"`
|
|
Failing []conditions.Condition `json:"failing"`
|
|
}
|
|
if err := json.Unmarshal(body, &doc); err != nil || len(doc.Failing) != 1 || len(doc.Conditions) != 1 ||
|
|
!strings.Contains(doc.Failing[0].Evidence[0].Said, "invalid_grant") {
|
|
t.Fatalf("the document does not carry it: %v\n%s", err, body)
|
|
}
|
|
// Both machines' `node show` name it: where the provider runs, and where the consumer is.
|
|
for _, node := range []string{"anchor", "laptop"} {
|
|
shown := printed(t, func() error { return showNode(ctx, open, node) })
|
|
if !strings.Contains(shown, "open condition(s) about this machine") || !strings.Contains(shown, "mesh_laptop_dashboard") {
|
|
t.Fatalf("node show %s does not name it:\n%s", node, shown)
|
|
}
|
|
}
|
|
|
|
// Recovered: gone, and the mesh may be well again as far as this is concerned.
|
|
failing.Failing = false
|
|
if cleared, err := kept.Stood(ctx, failing); err != nil || !cleared {
|
|
t.Fatalf("%v %v", cleared, err)
|
|
}
|
|
asked, err = theThreeQuestions(ctx, open)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(asked.conditions) != 0 {
|
|
t.Fatalf("a recovered consumer is still named: %+v", asked.conditions)
|
|
}
|
|
}
|
|
|
|
// A provider no longer assigned where it ran has nothing running to fail anybody: its last word is
|
|
// cleared on the next look, said as resolved by the assignment.
|
|
func TestAnUnassignedProvidersLastWordIsCleared(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
kept := standings{keeper: func() *conditions.Keeper { return conditionsFrom }}
|
|
if _, err := kept.Stood(ctx, link.Standing{Module: "gone", Failing: true,
|
|
ProviderNode: "anchor", Consumer: "x", Since: time.Now()}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
all, err := conditionsFrom.Open(ctx)
|
|
if err != nil || len(all) != 1 {
|
|
t.Fatalf("%+v %v", all, err)
|
|
}
|
|
if err := unassignedProviders(ctx, open.inventory, conditionsFrom, all); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if all, _ := conditionsFrom.Open(ctx); len(all) != 0 {
|
|
t.Fatalf("%+v", all)
|
|
}
|
|
}
|
|
|
|
// **The store away holds a recovery** (ADR 0224 §3): a standing that cannot be kept is asked again.
|
|
func TestAStandingTheStoreCannotKeepIsAskedAgain(t *testing.T) {
|
|
kept := standings{keeper: func() *conditions.Keeper { return nil }}
|
|
if _, err := kept.Stood(t.Context(), link.Standing{Module: "idp", ProviderNode: "anchor", Consumer: "x"}); err == nil ||
|
|
!strings.Contains(err.Error(), link.ErrTryAgain.Error()) {
|
|
t.Fatalf("answered %v", err)
|
|
}
|
|
}
|