Issue 258's fix let a mesh seat's holder elsewhere answer before this machine's own provider. Right for the resolver, which any provider answers alike; for the store's seat it re-bound every database consumer on a machine running its own store to the holder on another, each was given a fresh, empty database there, and nothing said so for twenty hours. - An offer says whether it keeps its consumers' data (`keeps-consumer-data`); unsaid, a provider that grants each consumer a credential does. For such a provision the seat's holder no longer overrules a provider beside the consumer; a pin still does. - Where each such consumer was sent is recorded (migration 0071). A resolution that would bind it elsewhere keeps the recorded provider and says the move; one whose provider is gone is refused, never answered by another. - A push says a kept move and raises it as an urgent condition at once; the self-check's D12 raises it every run, with a pinned move not yet sent as a warning and any unasked move as urgent.
258 lines
9.9 KiB
Go
258 lines
9.9 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A consumer of a provision that keeps its data stays bound where its data is (novox/hq ADR 0232,
|
|
// issue 273).
|
|
//
|
|
// The incident, exactly: a machine runs its own store and five consumers of it; the mesh's store seat
|
|
// is held by the store on another machine. Issue 258's rule — the seat's holder elsewhere answers
|
|
// before this machine's own provider — re-bound all five to the holder, each was made a fresh, empty
|
|
// database there, and nothing said so.
|
|
|
|
var incidentConsumers = []string{"board", "listings", "workflows", "agents", "game"}
|
|
|
|
func storeMesh() map[string]Manifest {
|
|
shelf := map[string]Manifest{
|
|
"store": {Module: "store", Version: "1",
|
|
Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh}},
|
|
Claims: []Claim{{Name: "mesh-store", Scope: ScopeMesh}},
|
|
Serves: map[string]map[string]any{"postgres-database": {"port": 5432}},
|
|
Grants: map[string]string{"postgres-database": "/grants"}},
|
|
"resolver": {Module: "resolver", Version: "1",
|
|
Provides: []Offer{{Name: "wildcard-resolution", Scope: ScopeMesh}},
|
|
Claims: []Claim{{Name: "mesh-dns-resolver", Scope: ScopeMesh}}},
|
|
"network": {Module: "network", Version: "1", Requires: []string{"wildcard-resolution"}},
|
|
}
|
|
for _, c := range incidentConsumers {
|
|
shelf[c] = Manifest{Module: c, Version: "1", Requires: []string{"postgres-database"}}
|
|
}
|
|
return shelf
|
|
}
|
|
|
|
// storeWorld is the rest of the mesh as the home server's plan sees it: the anchor runs a store and a
|
|
// resolver and holds both seats; the home server runs its own of each.
|
|
func storeWorld() World {
|
|
w := World{Offered: map[string][]Provider{
|
|
"postgres-database": {
|
|
{Node: "anchor", At: "anchor.internal", Module: "store", Serves: map[string]any{"port": 5432}},
|
|
{Node: "home", At: "home.internal", Module: "store", Serves: map[string]any{"port": 5434}},
|
|
},
|
|
"wildcard-resolution": {
|
|
{Node: "anchor", At: "anchor.internal", Module: "resolver"},
|
|
{Node: "home", At: "home.internal", Module: "resolver"},
|
|
},
|
|
}}
|
|
w.Held = []Held{
|
|
{Claim: "mesh-store", Scope: ScopeMesh, Node: "anchor", Module: "store"},
|
|
{Claim: "mesh-dns-resolver", Scope: ScopeMesh, Node: "anchor", Module: "resolver"},
|
|
}
|
|
w.Holdings = w.Held
|
|
return w
|
|
}
|
|
|
|
var home = Node{Name: "home", At: "home.internal"}
|
|
|
|
func homeAssigned() []string {
|
|
return append([]string{"store", "resolver", "network"}, incidentConsumers...)
|
|
}
|
|
|
|
func boundFrom(t *testing.T, r Resolution, consumer, provision string) Needed {
|
|
t.Helper()
|
|
for _, n := range r.Needs {
|
|
if n.For == consumer && n.Name == provision {
|
|
return n
|
|
}
|
|
}
|
|
t.Fatalf("no binding of %s for %s: %+v", consumer, provision, r.Needs)
|
|
return Needed{}
|
|
}
|
|
|
|
func TestTheIncidentAMachinesOwnStoreKeepsItsConsumersWhenTheSeatIsHeldElsewhere(t *testing.T) {
|
|
got, err := Resolve(storeMesh(), homeAssigned(), home, storeWorld())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, c := range incidentConsumers {
|
|
if n := boundFrom(t, got, c, "postgres-database"); n.From != "home" || n.Module != "store" {
|
|
t.Errorf("%s bound to %s/%s; its data is on the store beside it (issue 273)", c, n.From, n.Module)
|
|
}
|
|
}
|
|
// And the resolver, which keeps nothing of anybody's, still answers from the seat's holder (258).
|
|
if n := boundFrom(t, got, "network", "wildcard-resolution"); n.From != "anchor" {
|
|
t.Errorf("the resolver bound to %s; the seat is held on anchor (issue 258)", n.From)
|
|
}
|
|
if len(got.Kept) != 0 {
|
|
t.Errorf("nothing was moved, and %d kept: %+v", len(got.Kept), got.Kept)
|
|
}
|
|
}
|
|
|
|
func TestTheIncidentWithEveryConsumerOnRecordStillMovesNothing(t *testing.T) {
|
|
w := storeWorld()
|
|
w.Bound = map[string]map[string]Chosen{}
|
|
for _, c := range incidentConsumers {
|
|
w.Bound[c] = map[string]Chosen{"postgres-database": {Node: "home", Module: "store"}}
|
|
}
|
|
got, err := Resolve(storeMesh(), homeAssigned(), home, w)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, c := range incidentConsumers {
|
|
if n := boundFrom(t, got, c, "postgres-database"); n.From != "home" {
|
|
t.Errorf("%s bound to %s", c, n.From)
|
|
}
|
|
}
|
|
if len(got.Kept) != 0 {
|
|
t.Errorf("kept %+v", got.Kept)
|
|
}
|
|
}
|
|
|
|
func TestAPinElsewhereStillMovesAStoresConsumers(t *testing.T) {
|
|
w := storeWorld()
|
|
w.Pinned = map[string]Chosen{"postgres-database": {Node: "anchor", Module: "store"}}
|
|
w.Bound = map[string]map[string]Chosen{"board": {"postgres-database": {Node: "home", Module: "store"}}}
|
|
got, err := Resolve(storeMesh(), homeAssigned(), home, w)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := boundFrom(t, got, "board", "postgres-database"); n.From != "anchor" || n.Module != "store" {
|
|
t.Errorf("bound to %s/%s; a person pinned it to anchor", n.From, n.Module)
|
|
}
|
|
if len(got.Kept) != 0 {
|
|
t.Errorf("a pin is a person's move, not one to keep: %+v", got.Kept)
|
|
}
|
|
}
|
|
|
|
// A consumer on a machine with no store of its own, bound to one store, when the seat moves to
|
|
// another: the holder would answer, and the binding stays.
|
|
func laptopWorld(holder string) World {
|
|
w := storeWorld()
|
|
w.Held = []Held{{Claim: "mesh-store", Scope: ScopeMesh, Node: holder, Module: "store"}}
|
|
w.Holdings = w.Held
|
|
return w
|
|
}
|
|
|
|
var laptop = Node{Name: "laptop", At: "laptop.internal"}
|
|
|
|
func TestABoundConsumerStaysWhenTheSeatsHolderChanges(t *testing.T) {
|
|
w := laptopWorld("home")
|
|
w.Bound = map[string]map[string]Chosen{"board": {"postgres-database": {Node: "anchor", Module: "store"}}}
|
|
got, err := Resolve(storeMesh(), []string{"board"}, laptop, w)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
n := boundFrom(t, got, "board", "postgres-database")
|
|
if n.From != "anchor" || n.At != "anchor.internal" || n.Serves["port"] != 5432 {
|
|
t.Fatalf("bound to %s at %s %v; its data is on anchor", n.From, n.At, n.Serves)
|
|
}
|
|
if len(got.Kept) != 1 {
|
|
t.Fatalf("the move was not said: %+v", got.Kept)
|
|
}
|
|
k := got.Kept[0]
|
|
if k.Bound != (Chosen{"anchor", "store"}) || k.Would != (Chosen{"home", "store"}) || k.Consumer != "board" {
|
|
t.Fatalf("kept %+v", k)
|
|
}
|
|
for _, want := range []string{"would move board's postgres-database from anchor/store to home/store",
|
|
"its data is on anchor/store", "pin laptop postgres-database home store", "move the data first"} {
|
|
if !strings.Contains(k.String(), want) {
|
|
t.Errorf("%q does not say %q", k.String(), want)
|
|
}
|
|
}
|
|
// Without a record it is a binding being made, and the holder answers it as before.
|
|
w.Bound = nil
|
|
got, err = Resolve(storeMesh(), []string{"board"}, laptop, w)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := boundFrom(t, got, "board", "postgres-database"); n.From != "home" {
|
|
t.Errorf("a new consumer bound to %s; the seat is held on home", n.From)
|
|
}
|
|
}
|
|
|
|
func TestABoundConsumerWhoseProviderIsGoneIsRefusedNotMoved(t *testing.T) {
|
|
w := laptopWorld("anchor")
|
|
w.Offered["postgres-database"] = w.Offered["postgres-database"][:1] // only anchor's store is left
|
|
w.Bound = map[string]map[string]Chosen{"board": {"postgres-database": {Node: "home", Module: "store"}}}
|
|
_, err := Resolve(storeMesh(), []string{"board"}, laptop, w)
|
|
if err == nil {
|
|
t.Fatal("bound to home's store, which is gone, and answered by anchor's — the silent move")
|
|
}
|
|
for _, want := range []string{"board on laptop is bound to home/store", "home/store no longer provides it",
|
|
"pin laptop postgres-database anchor store"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("%q does not say %q", err, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAMachinesOwnStoreUnassignedRefusesItsBoundConsumers(t *testing.T) {
|
|
w := storeWorld()
|
|
w.Bound = map[string]map[string]Chosen{"board": {"postgres-database": {Node: "home", Module: "store"}}}
|
|
_, err := Resolve(storeMesh(), []string{"board"}, home, w)
|
|
if err == nil || !strings.Contains(err.Error(), "store no longer runs on home") {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
}
|
|
|
|
// The first pass asks only what a machine offers, and is never refused by a binding.
|
|
func TestTheFirstPassKeepsNoBinding(t *testing.T) {
|
|
w := storeWorld()
|
|
w.Unchecked = true
|
|
w.Bound = map[string]map[string]Chosen{"board": {"postgres-database": {Node: "gone", Module: "store"}}}
|
|
if _, err := Resolve(storeMesh(), []string{"board"}, laptop, w); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestAnOfferSaysWhetherItKeepsConsumerData(t *testing.T) {
|
|
var o Offer
|
|
if err := json.Unmarshal([]byte(`{"name":"wildcard-resolution","scope":"mesh","keeps-consumer-data":false}`), &o); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o.KeepsConsumerData == nil || *o.KeepsConsumerData {
|
|
t.Fatalf("read %+v", o)
|
|
}
|
|
out, err := json.Marshal(o)
|
|
if err != nil || !strings.Contains(string(out), `"keeps-consumer-data":false`) {
|
|
t.Fatalf("wrote %s, %v", out, err)
|
|
}
|
|
yes, no := true, false
|
|
granting := Manifest{Module: "g", Provides: []Offer{{Name: "p", Scope: ScopeMesh}}, Grants: map[string]string{"p": "/g"}}
|
|
if !granting.KeepsConsumerData("p") {
|
|
t.Error("a provider granting each consumer a credential keeps what it writes, unsaid")
|
|
}
|
|
if (Manifest{Module: "r", Provides: []Offer{{Name: "p", Scope: ScopeMesh}}}).KeepsConsumerData("p") {
|
|
t.Error("a provider granting nothing keeps nothing, unsaid")
|
|
}
|
|
granting.Provides[0].KeepsConsumerData = &no
|
|
if granting.KeepsConsumerData("p") {
|
|
t.Error("what an offer says, it gets")
|
|
}
|
|
said := Manifest{Module: "s", Provides: []Offer{{Name: "p", Scope: ScopeMesh, KeepsConsumerData: &yes}}}
|
|
if !said.KeepsConsumerData("p") || !KeepsConsumerData(map[string]Manifest{"s": said, "r": {Module: "r",
|
|
Provides: []Offer{{Name: "p", Scope: ScopeMesh, KeepsConsumerData: &no}}}}, "p") {
|
|
t.Error("a name any provider says keeps data keeps data")
|
|
}
|
|
}
|
|
|
|
// An offer saying it keeps nothing is answered by the seat's holder as the resolver is, even where it
|
|
// grants a credential.
|
|
func TestAStoreSayingItKeepsNothingFollowsTheSeat(t *testing.T) {
|
|
shelf := storeMesh()
|
|
no := false
|
|
s := shelf["store"]
|
|
s.Provides = []Offer{{Name: "postgres-database", Scope: ScopeMesh, KeepsConsumerData: &no}}
|
|
shelf["store"] = s
|
|
got, err := Resolve(shelf, homeAssigned(), home, storeWorld())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := boundFrom(t, got, "board", "postgres-database"); n.From != "anchor" {
|
|
t.Errorf("bound to %s; it keeps nothing, and the seat is held on anchor", n.From)
|
|
}
|
|
}
|