diff --git a/internal/catalogue/seats.go b/internal/catalogue/seats.go index afc80ddd..31abb4ea 100644 --- a/internal/catalogue/seats.go +++ b/internal/catalogue/seats.go @@ -2,7 +2,6 @@ package catalogue import ( "fmt" - "slices" "sort" "strings" ) @@ -54,14 +53,6 @@ type Seat struct { // ${contribution::}. Compiled, never stored: like the protocol, it is the mesh's // definition of the role, and the store's rows carry no column for it. Receives []Receivable - // Staged names verbs of Serves that are promised and routed but not yet a condition of holding the - // seat: a verb added to a seat whose holders do not serve it yet. Holding requires every verb, and a - // claim may name only promised ones (ADR 0132), so a verb required at once would refuse every holder - // until the catalogue serves it — and the catalogue could not name it until this controller ran. Staged, - // the controller rolls out first, the holders serve the verb next, and a later change requires it. - // Compiled, never stored, and kept out of the stored row's serves: a controller older than the verb - // reads the row strictly and would require what no holder of its time serves. - Staged []string // Decision is the record that made it a seat. Decision string } @@ -294,9 +285,8 @@ var defaultSeats = append([]Seat{ // Its verbs say what the machine resolves through and over which links (novox/hq ADR 0241): the // resolver file as it is and who wrote it, and every link with its addresses, routes and the resolvers // the manager knows for it — the same for every holder, so a question about a machine's names is asked - // the same way whatever manages its network. Staged until both holders serve them. - {Name: "node-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117", - Serves: uplinkVerbs(), Staged: []string{"resolvers", "links"}}, + // the same way whatever manages its network. Optional until both holders serve them. + {Name: "node-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117", Serves: uplinkVerbs()}, }, // The graphical session's roles (novox/hq ADR 0208), last because they are a workstation's. graphicalSessionSeats()...) @@ -353,14 +343,23 @@ func UseSeats(s []Seat) { if d, known := byName[row.Name]; known { row.Receives = d.Receives row.Replicated = d.Replicated - // Staged verbs are never in the row: they come from the binary that promises them. - row.Staged = d.Staged + // Optional verbs are never in the row (Verb.Optional): they come from the binary that promises + // them, still optional; and a row kept from before the mark keeps none of its own required. + optional := map[string]Verb{} + for _, v := range d.Serves { + if v.Optional { + optional[v.Name] = v + } + } have := map[string]bool{} - for _, v := range row.Serves { + for i, v := range row.Serves { have[v.Name] = true + if o, ok := optional[v.Name]; ok { + row.Serves[i] = o + } } for _, v := range d.Serves { - if slices.Contains(d.Staged, v.Name) && !have[v.Name] { + if v.Optional && !have[v.Name] { row.Serves = append(row.Serves, v) } } @@ -503,7 +502,7 @@ func CanHold(m Manifest, seat Seat) error { // **Serving the seat's tools is a condition of holding it** (novox/hq ADR 0132). A holder that // does not answer what the role promises is every caller's timeout, found at registration and // at handover instead, naming the verbs rather than the fact that something is missing. - if missing := unservedVerbs(claimed.ServesFor(m), seat.Required()); len(missing) > 0 { + if missing := unservedVerbs(claimed.ServesFor(m), seat.Serves); len(missing) > 0 { return fmt.Errorf("%s claims %s but does not serve %s, which that seat's protocol promises "+ "(novox/hq ADR 0132) — a holder names every verb its seat declares, under the claim's "+ "serves or among its own tools", @@ -617,20 +616,15 @@ func serviceManagerVerbs() []Verb { } } -// Required is the verbs a holder must serve: every one the seat promises but those still staged. -func (s Seat) Required() []Verb { - var out []Verb +// Stored is the seat as its row keeps it: its optional verbs left out (Verb.Optional). +func (s Seat) Stored() Seat { + var kept []Verb for _, v := range s.Serves { - if !slices.Contains(s.Staged, v.Name) { - out = append(out, v) + if !v.Optional { + kept = append(kept, v) } } - return out -} - -// Stored is the seat as its row keeps it: its staged verbs left out (see Staged). -func (s Seat) Stored() Seat { - s.Serves = s.Required() + s.Serves = kept return s } @@ -642,11 +636,11 @@ func uplinkVerbs() []Verb { "domains and options it lists, whether it is the file the mesh declares, and when it is not, who " + "wrote it as far as the machine shows — its header, a backup a VPN client left beside it, a link " + "in its place, a known writer running.", - Input: schema(nil, nil)}, + Input: schema(nil, nil), Optional: true}, {Name: "links", Description: "Every network link on the machine: its state, its addresses, whether " + "the default route leaves through it, and the resolvers and search domains the network manager " + "knows for it — a VPN's tunnel included.", - Input: schema(nil, nil)}, + Input: schema(nil, nil), Optional: true}, } } diff --git a/internal/catalogue/uplink_verbs_test.go b/internal/catalogue/uplink_verbs_test.go index ae83e351..7c1e9307 100644 --- a/internal/catalogue/uplink_verbs_test.go +++ b/internal/catalogue/uplink_verbs_test.go @@ -6,8 +6,10 @@ import ( "testing" ) -// The node-uplink seat's verbs (novox/hq ADR 0241): promised and routed, staged — not yet a condition of +// The node-uplink seat's verbs (novox/hq ADR 0241): promised and routed, optional — not yet a condition of // holding — so the holders of today still hold it, a holder may name them, and nothing it does not promise. +// And an optional verb is never stored in the seat's row (Verb.Optional): a controller that reads the row +// back must still find it optional, and one older than the verb must not find it at all. func uplinkSeat(t *testing.T) Seat { t.Helper() @@ -25,9 +27,12 @@ func TestTheUplinkSeatPromisesItsVerbsAndRequiresNoneYet(t *testing.T) { var names []string for _, v := range seat.Serves { names = append(names, v.Name) + if !v.Optional { + t.Errorf("%s is required already", v.Name) + } } - if !slices.Equal(names, []string{"resolvers", "links"}) || len(seat.Required()) != 0 { - t.Fatalf("node-uplink promises %v and requires %v", names, seat.Required()) + if !slices.Equal(names, []string{"resolvers", "links"}) { + t.Fatalf("node-uplink promises %v", names) } today := Manifest{Module: "networkmanager", Version: "1", Claims: []Claim{{Name: "node-uplink", Scope: ScopeNode}}} if err := CanHold(today, seat); err != nil { @@ -45,24 +50,44 @@ func TestTheUplinkSeatPromisesItsVerbsAndRequiresNoneYet(t *testing.T) { } } -func TestAStagedVerbIsNeverStoredAndComesBackFromTheBinary(t *testing.T) { +func TestAnOptionalVerbIsNeverStoredAndComesBackOptionalFromTheBinary(t *testing.T) { seat := uplinkSeat(t) if stored := seat.Stored(); len(stored.Serves) != 0 { t.Fatalf("the row would keep %v", stored.Serves) } before := seats t.Cleanup(func() { seats = before }) - // A row as the store keeps it: no protocol of its own. And one an operator gave a verb of its own. + optionalBoth := func(want int) { + t.Helper() + for _, s := range Seats() { + if s.Name != "node-uplink" { + continue + } + if len(s.Serves) != want { + t.Fatalf("the working set's node-uplink serves %+v", s.Serves) + } + holder := Manifest{Module: "networkmanager", Version: "1", Claims: []Claim{{Name: "node-uplink", Scope: ScopeNode}}} + for _, v := range s.Serves { + if v.Name != "operators-own" { + if !v.Optional { + t.Fatalf("%s came back required", v.Name) + } + } else { + holder.Claims[0].Serves = []string{"operators-own"} + } + } + if err := CanHold(holder, s); err != nil { + t.Fatalf("a holder serving none of the optional verbs is refused: %v", err) + } + } + } + // A row as the store keeps it: no protocol of its own. UseSeats([]Seat{{Name: "node-uplink", Scope: ScopeNode}, {Name: "node-service-manager", Scope: ScopeNode}}) - for _, s := range Seats() { - if s.Name == "node-uplink" && (len(s.Serves) != 2 || len(s.Staged) != 2) { - t.Fatalf("the working set's node-uplink is %+v", s) - } - } + optionalBoth(2) + // A row with a verb an operator added: it keeps it and gains the optional ones. UseSeats([]Seat{{Name: "node-uplink", Scope: ScopeNode, Serves: []Verb{{Name: "operators-own"}}}}) - for _, s := range Seats() { - if s.Name == "node-uplink" && len(s.Serves) != 3 { - t.Fatalf("a row with a verb of its own lost or did not gain the staged ones: %+v", s.Serves) - } - } + optionalBoth(3) + // A row that stored the verb before it was marked (or by an older seeding): read back, still optional. + UseSeats([]Seat{{Name: "node-uplink", Scope: ScopeNode, Serves: []Verb{{Name: "resolvers"}, {Name: "links"}}}}) + optionalBoth(2) } diff --git a/internal/catalogue/verbs.go b/internal/catalogue/verbs.go index af3318fd..e025ba72 100644 --- a/internal/catalogue/verbs.go +++ b/internal/catalogue/verbs.go @@ -28,7 +28,9 @@ type Verb struct { // does not serve the verb, the controller before it refusing the holder that does — and every check of // the catalogue between the two would fail on a module nobody touched. Once every holder serves it, // the mark is removed and the verb is a condition of holding like the rest. Never stored or said: the - // seat set's protocol is the compiled one. + // seat's row leaves an optional verb out (Seat.Stored) and the working set takes it, still optional, + // from the compiled seat (UseSeats) — the mark itself is not a column, so a stored verb would read back + // required, and a controller older than the verb would require it of every holder (novox/hq ADR 0241). Optional bool `json:"-"` } diff --git a/internal/inventory/seats.go b/internal/inventory/seats.go index 0832e6b7..b12503eb 100644 --- a/internal/inventory/seats.go +++ b/internal/inventory/seats.go @@ -62,7 +62,7 @@ func (i *Inventory) Seats(ctx context.Context) ([]catalogue.Seat, error) { func (i *Inventory) SeedSeats(ctx context.Context, defaults []catalogue.Seat) (int, error) { var added int for _, s := range defaults { - // A verb still staged is never stored (catalogue.Seat.Staged). + // An optional verb is never stored (catalogue.Verb.Optional). s = s.Stored() accepts, emits, serves, err := protocolJSON(s) if err != nil { diff --git a/internal/inventory/seats_test.go b/internal/inventory/seats_test.go index 94594362..168eed62 100644 --- a/internal/inventory/seats_test.go +++ b/internal/inventory/seats_test.go @@ -157,9 +157,10 @@ func TestTheControllersVerbsInTheRowAreTheBinarys(t *testing.T) { } } -// A verb still staged (novox/hq ADR 0241) is never in the store's row: a controller older than it reads -// the row and would require what no holder of its time serves. -func TestAStagedVerbIsNotSeeded(t *testing.T) { +// An optional verb (catalogue.Verb.Optional; novox/hq ADR 0241, ADR 0239) is never in the store's row: a +// controller older than it reads the row and would require what no holder of its time serves, and the mark +// itself is not stored, so the row would read back required. +func TestAnOptionalVerbIsNotSeeded(t *testing.T) { inv := ForTest(t) if _, err := inv.SeedSeats(t.Context(), catalogue.DefaultSeats()); err != nil { t.Fatal(err) @@ -173,8 +174,10 @@ func TestAStagedVerbIsNotSeeded(t *testing.T) { t.Fatal(err) } for _, s := range got { - if s.Name == "node-uplink" && len(s.Serves) != 0 { - t.Fatalf("the row keeps %v", s.Serves) + for _, v := range s.Serves { + if (s.Name == "node-uplink") || (s.Name == catalogue.DeliverySeat && v.Name == "checks") { + t.Fatalf("%s's row keeps the optional %s", s.Name, v.Name) + } } } }