The identity provider failed every consumer for a day and status called the mesh well (hq issue 179). The controller now follows every provider's provisioner.failing/recovered, keeps the newest failing word per provider, machine and consumer (migration 0065), and status, its JSON and node show name it until it recovers. Every module that receives contributions is granted the two events, so no manifest can forget them.
204 lines
6.8 KiB
Go
204 lines
6.8 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
)
|
|
|
|
// A provider's standing (novox/hq ADR 0224): who said it is read from the subject the bus let it
|
|
// publish on, a failing one is kept and a recovery cleared, and a recovery is never lost to a store
|
|
// that is away — said once, it would leave status naming a consumer that is fine.
|
|
|
|
type keptStandings struct {
|
|
kept []Standing
|
|
err error
|
|
}
|
|
|
|
func (k *keptStandings) Stood(_ context.Context, s Standing) (bool, error) {
|
|
if k.err != nil {
|
|
return false, k.err
|
|
}
|
|
k.kept = append(k.kept, s)
|
|
return !s.Failing, nil
|
|
}
|
|
|
|
func standingSays(t *testing.T, in *fakeInbound, to *settled, subject string, body map[string]any) Control {
|
|
t.Helper()
|
|
m := in.sends(t, to, KindProvisioner, body).(*fakeControl)
|
|
m.subject = subject
|
|
return m
|
|
}
|
|
|
|
func TestTheControllerFollowsEveryProvidersStandingAndNothingElse(t *testing.T) {
|
|
for subject, want := range map[string]string{
|
|
"mesh.mod.keycloak.event.provisioner.failing": "keycloak",
|
|
"mesh.mod.postgres.event.provisioner.recovered": "postgres",
|
|
"mesh.mod.*.event.provisioner.failing": "*",
|
|
} {
|
|
got, ok := ProvisionerEmitter(subject)
|
|
if !ok || got != want {
|
|
t.Errorf("%s: %q %v", subject, got, ok)
|
|
}
|
|
if kind, _ := kindOfSubject(subject); kind != KindProvisioner {
|
|
t.Errorf("%s decodes to %q", subject, kind)
|
|
}
|
|
}
|
|
for _, subject := range []string{
|
|
"mesh.mod.keycloak.event.provisioner.other",
|
|
"mesh.mod.keycloak.event.client.created",
|
|
"mesh.mod.a.b.event.provisioner.failing",
|
|
"mesh.seat.keycloak.event.provisioner.failing",
|
|
} {
|
|
if _, ok := ProvisionerEmitter(subject); ok {
|
|
t.Errorf("%s read as a provider's standing", subject)
|
|
}
|
|
}
|
|
var follows int
|
|
for _, s := range broker.ControllerFollows {
|
|
if kind, _ := kindOfSubject(s); kind == KindProvisioner {
|
|
follows++
|
|
}
|
|
}
|
|
if follows != 2 {
|
|
t.Fatalf("the controller follows %d standing subjects, want failing and recovered", follows)
|
|
}
|
|
}
|
|
|
|
func TestAFailingStandingIsKeptNamingTheEmitterFromTheSubject(t *testing.T) {
|
|
s, in := serving()
|
|
kept := &keptStandings{}
|
|
if err := s.Watches(kept); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
to := &settled{}
|
|
since := time.Date(2026, 10, 5, 0, 49, 0, 0, time.UTC)
|
|
s.act(t.Context(), standingSays(t, in, to, "mesh.mod.keycloak.event.provisioner.failing", map[string]any{
|
|
"provider": "oidc-client", "provider-node": "anchor", "consumer": "mesh_home_grafana",
|
|
"node": "home-server", "class": "credentials-rejected", "error": "401 invalid_grant",
|
|
"since": since, "attempts": 31000,
|
|
// A body naming another module is not believed: the subject is the bus's word.
|
|
"module": "postgres",
|
|
}))
|
|
if !to.acked || len(kept.kept) != 1 {
|
|
t.Fatalf("settled %+v, kept %+v", to, kept.kept)
|
|
}
|
|
got := kept.kept[0]
|
|
if got.Module != "keycloak" || !got.Failing || got.Consumer != "mesh_home_grafana" || got.Node != "home-server" ||
|
|
got.ProviderNode != "anchor" || got.Class != "credentials-rejected" || got.Attempts != 31000 || !got.Since.Equal(since) {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
|
|
to = &settled{}
|
|
s.act(t.Context(), standingSays(t, in, to, "mesh.mod.keycloak.event.provisioner.recovered", map[string]any{
|
|
"provider": "oidc-client", "provider-node": "anchor", "consumer": "mesh_home_grafana",
|
|
}))
|
|
if !to.acked || len(kept.kept) != 2 || kept.kept[1].Failing {
|
|
t.Fatalf("settled %+v, kept %+v", to, kept.kept)
|
|
}
|
|
}
|
|
|
|
func TestARecoveryIsHeldWhileTheStoreIsAway(t *testing.T) {
|
|
s, in := serving()
|
|
kept := &keptStandings{err: restarting}
|
|
if err := s.Watches(kept); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
to := &settled{}
|
|
s.act(t.Context(), standingSays(t, in, to, "mesh.mod.keycloak.event.provisioner.recovered",
|
|
map[string]any{"consumer": "mesh_home_grafana"}))
|
|
if !to.unsettled() || len(in.held) != 1 {
|
|
t.Fatalf("a recovery was settled while the store was away: %+v", to)
|
|
}
|
|
kept.err = nil
|
|
in.retries(t.Context(), s)
|
|
if !to.acked || len(kept.kept) != 1 {
|
|
t.Fatalf("the held recovery was not kept when the store came back: %+v %+v", to, kept.kept)
|
|
}
|
|
}
|
|
|
|
func TestAStandingThatNamesNoConsumerIsTakenAndForgotten(t *testing.T) {
|
|
s, in := serving()
|
|
kept := &keptStandings{}
|
|
if err := s.Watches(kept); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
to := &settled{}
|
|
s.act(t.Context(), standingSays(t, in, to, "mesh.mod.keycloak.event.provisioner.failing", map[string]any{}))
|
|
if !to.acked || len(kept.kept) != 0 {
|
|
t.Fatalf("%+v %+v", to, kept.kept)
|
|
}
|
|
}
|
|
|
|
func TestAStandingWithNothingKeepingItIsTaken(t *testing.T) {
|
|
s, in := serving()
|
|
to := &settled{}
|
|
s.act(t.Context(), standingSays(t, in, to, "mesh.mod.keycloak.event.provisioner.failing",
|
|
map[string]any{"consumer": "x"}))
|
|
if !to.acked {
|
|
t.Fatal("a standing nothing keeps was left for the bus to hand over again")
|
|
}
|
|
}
|
|
|
|
// Over a real bus: a provider's standing published under its own module's namespace reaches the
|
|
// controller through the events consumer's filter — the one wildcard filter on it — names the emitter
|
|
// from the subject, and is acknowledged.
|
|
func TestNatsAProvidersStandingReachesTheController(t *testing.T) {
|
|
js := aBus(t)
|
|
kept := &lockedStandings{}
|
|
s := &Server{inbound: Nats(js), bus: OverNATS{Conn: js.Conn(), JS: js.Context()}, log: quiet()}
|
|
if err := s.Follows(&toldAbout{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.Watches(kept); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, stop := context.WithCancel(context.Background())
|
|
defer stop()
|
|
go func() { _ = s.Serve(ctx) }()
|
|
eventually(t, "the controller's event consumer being made", func() bool {
|
|
_, err := js.Context().ConsumerInfo("EVENTS", broker.ControllerName)
|
|
return err == nil
|
|
})
|
|
|
|
for _, event := range []string{broker.ProvisionerFailing, broker.ProvisionerRecovered} {
|
|
if _, err := js.Context().Publish("mesh.mod.keycloak.event."+event,
|
|
[]byte(`{"consumer":"mesh_home_grafana","provider-node":"anchor","class":"credentials-rejected"}`)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// Somebody else's event under the same prefix is not the controller's to hear.
|
|
if _, err := js.Context().Publish("mesh.mod.keycloak.event.client.created", []byte(`{}`)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
eventually(t, "both standings being kept, in order, naming the emitter", func() bool {
|
|
got := kept.all()
|
|
return len(got) == 2 && got[0].Module == "keycloak" && got[0].Failing && !got[1].Failing
|
|
})
|
|
eventually(t, "both being acknowledged and nothing else delivered", func() bool {
|
|
info, err := js.Context().ConsumerInfo("EVENTS", broker.ControllerName)
|
|
return err == nil && info.NumAckPending == 0 && info.Delivered.Consumer == 2
|
|
})
|
|
}
|
|
|
|
type lockedStandings struct {
|
|
mu sync.Mutex
|
|
kept []Standing
|
|
}
|
|
|
|
func (l *lockedStandings) Stood(_ context.Context, s Standing) (bool, error) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.kept = append(l.kept, s)
|
|
return !s.Failing, nil
|
|
}
|
|
|
|
func (l *lockedStandings) all() []Standing {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return append([]Standing(nil), l.kept...)
|
|
}
|