The seat set was a Go slice compiled into the controller and referenced by name everywhere, so changing it meant a rebuild and a freeze-prone deploy. It is now a table: catalogue keeps the shipped set as defaultSeats (the seed and the fallback) and a loadable working set; inventory adds the seat table (migration 0034), Seats to read it, and SeedSeats to fill it idempotently without overwriting an operator's edit; migrate seeds it; openInventory loads it, and an empty or unreadable table leaves the compiled defaults in force so it can never brick the control plane's boot. Behaviour-neutral: the seeded table equals the defaults. Phase 2 (reference by a stable id so a rename touches no manifest or code, and the builder reads the set from the mesh) follows.
269 lines
11 KiB
Go
269 lines
11 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0110: a seat is a module assignment from a closed set.
|
|
|
|
// The set is closed, and changing it is a decision.
|
|
//
|
|
// **The count is asserted, and every entry names the record that made it a seat**, so the next
|
|
// person changing the set finds the argument rather than a number to edit — the pattern the host's
|
|
// vocabulary test follows. If this fails because a seat was added, the fix is a record in novox/hq
|
|
// and a row in to-be 26, not a new number here.
|
|
func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
|
record := regexp.MustCompile(`^novox/hq ADR \d{4}$`)
|
|
seen := map[string]bool{}
|
|
delivered := map[string]string{}
|
|
for _, s := range Seats() {
|
|
if seen[s.Name] {
|
|
t.Errorf("%s is in the set twice", s.Name)
|
|
}
|
|
seen[s.Name] = true
|
|
if !record.MatchString(s.Decision) {
|
|
t.Errorf("%s names %q as its decision; every seat names the record that made it one",
|
|
s.Name, s.Decision)
|
|
}
|
|
switch s.Scope {
|
|
case ScopeNode, ScopeSite, ScopeMesh:
|
|
default:
|
|
t.Errorf("%s is held per %q, which is not a scope", s.Name, s.Scope)
|
|
}
|
|
if s.Delivers != "" {
|
|
// Two seats answering for one provision would put the question "which one?" back,
|
|
// which is the question a seat exists to answer.
|
|
if other, twice := delivered[s.Delivers]; twice {
|
|
t.Errorf("%s and %s both deliver %q", other, s.Name, s.Delivers)
|
|
}
|
|
delivered[s.Delivers] = s.Name
|
|
}
|
|
}
|
|
if len(Seats()) != 14 {
|
|
t.Errorf("the mesh defines %d seats rather than 14; the set is closed, so a change here is "+
|
|
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0117: a machine's uplink is a seat, held per machine, and delivers nothing.
|
|
//
|
|
// **Nothing, because nothing may be required of it.** A holder only keeps its network manager from
|
|
// contradicting the mesh; a requirement resolving to it would make the manager the mesh's answer
|
|
// for something, and the manager's link is the one thing the mesh must never be able to break.
|
|
func TestTheUplinkIsANodeSeatThatDeliversNothing(t *testing.T) {
|
|
seat, known := SeatNamed("node-uplink")
|
|
if !known {
|
|
t.Fatalf("the uplink is not a seat; the seats are: %s", seatNames())
|
|
}
|
|
if seat.Scope != ScopeNode || seat.Delivers != "" || seat.Decision != "novox/hq ADR 0117" {
|
|
t.Fatalf("the uplink is %+v, not a node seat delivering nothing by ADR 0117", seat)
|
|
}
|
|
// And a manager's module can hold it without providing anything.
|
|
raw := []byte(`{"module":"networkmanager","version":"1","claims":[{"name":"node-uplink","scope":"node"}]}`)
|
|
if _, err := ParseManifest(raw); err != nil {
|
|
t.Fatalf("a network manager's module could not hold the uplink: %v", err)
|
|
}
|
|
}
|
|
|
|
func claimed(claims string) []byte {
|
|
return []byte(`{"module":"thing","version":"1","provides":[{"name":"npm-package-registry","scope":"mesh"}],"claims":` + claims + `}`)
|
|
}
|
|
|
|
func TestAClaimOnASeatTheMeshDoesNotDefineIsRefused(t *testing.T) {
|
|
_, err := ParseManifest(claimed(`[{"name":"the-anything","scope":"node"}]`))
|
|
if err == nil {
|
|
t.Fatal("a module invented a seat by claiming it")
|
|
}
|
|
if !strings.Contains(err.Error(), "the-anything") || !strings.Contains(err.Error(), "not a seat") {
|
|
t.Fatalf("the refusal does not say the seat is unknown: %v", err)
|
|
}
|
|
// And it says what the seats are, because "no" without the list sends somebody reading code.
|
|
if !strings.Contains(err.Error(), "node-packet-filter") {
|
|
t.Fatalf("the refusal does not list the seats: %v", err)
|
|
}
|
|
}
|
|
|
|
// A module may define its own seat and claim it — the mesh enforces exclusivity without knowing
|
|
// what it means (novox/hq ADR 0121). But it may not define one in the mesh's own namespace.
|
|
func TestAModuleDefinesAndClaimsItsOwnSeat(t *testing.T) {
|
|
ok := []byte(`{"module":"showcase","version":"1","seats":[{"name":"the-showcase","scope":"node"}],` +
|
|
`"claims":[{"name":"the-showcase","scope":"node"}]}`)
|
|
if _, err := ParseManifest(ok); err != nil {
|
|
t.Fatalf("a module could not define and claim its own seat: %v", err)
|
|
}
|
|
// Claiming a name it neither the mesh nor the module defines is still refused.
|
|
if _, err := ParseManifest(claimed(`[{"name":"the-anything","scope":"node"}]`)); err == nil {
|
|
t.Fatal("a module claimed a seat nobody defines")
|
|
}
|
|
// A module may not carve its seat out of the mesh's own namespace.
|
|
bad := []byte(`{"module":"x","version":"1","seats":[{"name":"node-mine","scope":"node"}],` +
|
|
`"claims":[{"name":"node-mine","scope":"node"}]}`)
|
|
if _, err := ParseManifest(bad); err == nil || !strings.Contains(err.Error(), "own namespace") {
|
|
t.Fatalf("a module defined a seat in the mesh's namespace and was not refused: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestASeatClaimedAtAnotherScopeIsRefused(t *testing.T) {
|
|
_, err := ParseManifest(claimed(`[{"name":"npm-package-registry","scope":"node"}]`))
|
|
if err == nil {
|
|
t.Fatal("a mesh seat was held per node")
|
|
}
|
|
if !strings.Contains(err.Error(), "mesh seat") {
|
|
t.Fatalf("the refusal does not say which scope the seat is: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestADeliveringSeatIsOnlyHeldByAModuleThatProvides(t *testing.T) {
|
|
// Holding it makes the module the mesh's answer for the provision. A module that cannot answer
|
|
// would be the answer anyway, and every consumer would be sent to it.
|
|
raw := []byte(`{"module":"thing","version":"1","claims":[{"name":"git","scope":"mesh"}]}`)
|
|
_, err := ParseManifest(raw)
|
|
if err == nil {
|
|
t.Fatal("a module holding the git seat need not provide git")
|
|
}
|
|
if !strings.Contains(err.Error(), `does not provide "git"`) {
|
|
t.Fatalf("the refusal does not say what is missing: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAClaimThatIsMalformedIsRefusedOnceForThat(t *testing.T) {
|
|
// Not a second time for being unknown: one mistake, one line.
|
|
_, err := ParseManifest(claimed(`[{"name":"Not A Name","scope":"node"}]`))
|
|
if err == nil {
|
|
t.Fatal("a malformed claim was accepted")
|
|
}
|
|
if strings.Contains(err.Error(), "not a seat") {
|
|
t.Fatalf("a malformed claim was also called unknown: %v", err)
|
|
}
|
|
}
|
|
|
|
// Every module in use claims a seat in the set, so closing it refuses nothing that runs.
|
|
//
|
|
// Read from the catalogue beside this checkout and from this repository's own manifest, the two
|
|
// places a manifest lives (ADR 0069). The private-network module's manifest is composed in code,
|
|
// and its claim is checked where it is composed.
|
|
func TestEveryManifestInUseClaimsASeatTheMeshDefines(t *testing.T) {
|
|
paths, _ := filepath.Glob("../../../mesh-catalog/modules/*/module.json")
|
|
if len(paths) == 0 {
|
|
t.Skip("the catalogue is not beside this checkout")
|
|
}
|
|
paths = append(paths, "../../module.json")
|
|
var checked int
|
|
for _, path := range paths {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Leniently, so a manifest refused for something unrelated is not reported as a seat
|
|
// problem, and the seat check below is the only thing this test holds a module to.
|
|
var m Manifest
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatalf("%s: %v", path, err)
|
|
}
|
|
for _, problem := range claimProblems(m) {
|
|
t.Errorf("%s: %s", path, problem)
|
|
}
|
|
checked += len(m.Claims)
|
|
}
|
|
if checked == 0 {
|
|
t.Fatal("no claims were checked, so this proved nothing")
|
|
}
|
|
}
|
|
|
|
// The holder of a seat answers among several providers.
|
|
|
|
func registryShelf() map[string]Manifest {
|
|
return shelf(
|
|
Manifest{Module: "gitea", Version: "1", Provides: FromAnywhere("npm-package-registry"),
|
|
Claims: []Claim{{Name: "npm-package-registry", Scope: ScopeMesh}}},
|
|
Manifest{Module: "verdaccio", Version: "1", Provides: FromAnywhere("npm-package-registry")},
|
|
Manifest{Module: "builder", Version: "1", Requires: []string{"npm-package-registry"}},
|
|
)
|
|
}
|
|
|
|
func twoRegistries() map[string][]Provider {
|
|
return map[string][]Provider{"npm-package-registry": {
|
|
{Node: "anchor", At: "anchor.internal", Module: "gitea"},
|
|
{Node: "archive", At: "archive.internal", Module: "verdaccio"},
|
|
}}
|
|
}
|
|
|
|
func giteaHoldsTheSeat() []Held {
|
|
return []Held{{Claim: "npm-package-registry", Scope: ScopeMesh, Node: "anchor", Module: "gitea"}}
|
|
}
|
|
|
|
func TestTheSeatsHolderAnswersWhenSeveralProvide(t *testing.T) {
|
|
// The whole point: a second registry beside the holder harms nothing, and nobody pins.
|
|
got, err := Resolve(registryShelf(), []string{"builder"}, reachable(),
|
|
World{Offered: twoRegistries(), Held: giteaHoldsTheSeat()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Needs) != 1 || got.Needs[0].From != "anchor" {
|
|
t.Fatalf("the seat's holder did not answer: %v", got.Needs)
|
|
}
|
|
}
|
|
|
|
func TestAPinStillWinsOverTheSeat(t *testing.T) {
|
|
// A consumer coupled to one provider's contents has said so, and the seat does not overrule it.
|
|
got, err := Resolve(registryShelf(), []string{"builder"}, reachable(),
|
|
World{Offered: twoRegistries(), Held: giteaHoldsTheSeat(),
|
|
Pinned: map[string]string{"npm-package-registry": "archive"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Needs) != 1 || got.Needs[0].From != "archive" {
|
|
t.Fatalf("the pin was overruled by the seat: %v", got.Needs)
|
|
}
|
|
}
|
|
|
|
func TestWithTheSeatUnheldSeveralProvidersAreStillRefused(t *testing.T) {
|
|
// No seat held is no choice made, and ADR 0009's rule stands: never guessed.
|
|
_, err := Resolve(registryShelf(), []string{"builder"}, reachable(),
|
|
World{Offered: twoRegistries()})
|
|
if err == nil {
|
|
t.Fatal("one of two registries was picked with nobody holding the seat")
|
|
}
|
|
if !strings.Contains(err.Error(), "pin") {
|
|
t.Fatalf("the refusal does not say how to choose: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTheHolderIsTheModuleNotTheMachine(t *testing.T) {
|
|
// Two modules on one machine could provide the same thing; only the one holding the seat
|
|
// answers. A holder matched by node alone would send consumers to whichever came first.
|
|
providers := []Provider{
|
|
{Node: "anchor", At: "anchor.internal", Module: "verdaccio"},
|
|
{Node: "anchor", At: "anchor.internal", Module: "gitea"},
|
|
}
|
|
holder, held := HolderAmong("npm-package-registry", providers, giteaHoldsTheSeat())
|
|
if !held || holder.Module != "gitea" {
|
|
t.Fatalf("the holder was not told apart from a neighbour: %+v", holder)
|
|
}
|
|
}
|
|
|
|
// The working set is loaded from the store, and an empty load never erases it (novox/hq ADR 0122).
|
|
func TestUseSeatsReplacesTheSetButNeverEmptiesIt(t *testing.T) {
|
|
before := Seats()
|
|
defer UseSeats(DefaultSeats()) // restore for other tests, whatever this leaves it as
|
|
|
|
// An empty load (store not seeded, or unreadable) leaves the compiled defaults in force.
|
|
UseSeats(nil)
|
|
if len(Seats()) != len(before) {
|
|
t.Fatalf("an empty load changed the set from %d to %d seats", len(before), len(Seats()))
|
|
}
|
|
// A non-empty load replaces it — this is how a rename in the store reaches the lookups.
|
|
UseSeats([]Seat{{Name: "node-firewall", Scope: ScopeNode, Decision: "novox/hq ADR 0122"}})
|
|
if _, known := SeatNamed("node-firewall"); !known {
|
|
t.Fatal("the loaded set did not replace the working set")
|
|
}
|
|
if len(Seats()) != 1 {
|
|
t.Fatalf("the working set is %d seats, not the one that was loaded", len(Seats()))
|
|
}
|
|
}
|