The mesh's own verbs are the mesh-controller seat's tools
A seat's protocol lives in the store (migration 0047; seeded additively), a served verb carries its description and schema, holding a mesh seat requires serving its verbs, a node-scoped seat's tool carries the node, and the control plane serves status, nodes, node, modules, seats, builds, plan, assign, unassign, push, build and tools on its seat by running the same commands (novox/hq ADR 0132, ADR 0154, design 33). A grant of * reaches a role's tools; seat:<seat>.<verb> grants one.
This commit is contained in:
+115
-7
@@ -2,6 +2,7 @@ package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
// Seats is every seat the mesh defines, read from the store.
|
||||
func (i *Inventory) Seats(ctx context.Context) ([]catalogue.Seat, error) {
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select name, scope, delivers, decided from seat order by name`)
|
||||
`select name, scope, delivers, decided, accepts, emits, serves from seat order by name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -26,9 +27,21 @@ func (i *Inventory) Seats(ctx context.Context) ([]catalogue.Seat, error) {
|
||||
var seats []catalogue.Seat
|
||||
for rows.Next() {
|
||||
var s catalogue.Seat
|
||||
if err := rows.Scan(&s.Name, &s.Scope, &s.Delivers, &s.Decision); err != nil {
|
||||
var accepts, emits, serves []byte
|
||||
if err := rows.Scan(&s.Name, &s.Scope, &s.Delivers, &s.Decision, &accepts, &emits, &serves); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The protocol, from the row (novox/hq ADR 0132). A row that predates the columns has empty
|
||||
// lists, and UseSeats keeps the compiled protocol for it until the next seeding fills them.
|
||||
if err := json.Unmarshal(accepts, &s.Accepts); err != nil {
|
||||
return nil, fmt.Errorf("seat %s: accepts: %w", s.Name, err)
|
||||
}
|
||||
if err := json.Unmarshal(emits, &s.Emits); err != nil {
|
||||
return nil, fmt.Errorf("seat %s: emits: %w", s.Name, err)
|
||||
}
|
||||
if err := json.Unmarshal(serves, &s.Serves); err != nil {
|
||||
return nil, fmt.Errorf("seat %s: serves: %w", s.Name, err)
|
||||
}
|
||||
seats = append(seats, s)
|
||||
}
|
||||
return seats, rows.Err()
|
||||
@@ -41,21 +54,116 @@ func (i *Inventory) Seats(ctx context.Context) ([]catalogue.Seat, error) {
|
||||
// exactly as it is, so an operator's rename in the table is not undone by the next deploy putting
|
||||
// the old name back. What a release removes from the defaults is not deleted here either; retiring a
|
||||
// seat is its own decision, not a silent consequence of it dropping out of the binary.
|
||||
//
|
||||
// **The protocol is seeded additively** (novox/hq ADR 0132, design 33 §7). A row that has none takes
|
||||
// the compiled protocol whole — that is the compiled fallback becoming data, once. A row that has one
|
||||
// gains any verb the defaults name and it lacks, and loses nothing: a seat's tools are an interface,
|
||||
// additive within a version, and a verb an operator added to the row is theirs to keep.
|
||||
func (i *Inventory) SeedSeats(ctx context.Context, defaults []catalogue.Seat) (int, error) {
|
||||
var added int
|
||||
for _, s := range defaults {
|
||||
tag, err := i.store.Pool().Exec(ctx,
|
||||
`insert into seat (name, scope, delivers, decided) values ($1, $2, $3, $4)
|
||||
on conflict (name) do nothing`,
|
||||
s.Name, s.Scope, s.Delivers, s.Decision)
|
||||
accepts, emits, serves, err := protocolJSON(s)
|
||||
if err != nil {
|
||||
return added, err
|
||||
}
|
||||
added += int(tag.RowsAffected())
|
||||
tag, err := i.store.Pool().Exec(ctx,
|
||||
`insert into seat (name, scope, delivers, decided, accepts, emits, serves)
|
||||
values ($1, $2, $3, $4, $5, $6, $7)
|
||||
on conflict (name) do nothing`,
|
||||
s.Name, s.Scope, s.Delivers, s.Decision, accepts, emits, serves)
|
||||
if err != nil {
|
||||
return added, err
|
||||
}
|
||||
if n := int(tag.RowsAffected()); n > 0 {
|
||||
added += n
|
||||
continue
|
||||
}
|
||||
if err := i.widenProtocol(ctx, s); err != nil {
|
||||
return added, err
|
||||
}
|
||||
}
|
||||
return added, nil
|
||||
}
|
||||
|
||||
// widenProtocol adds to a seat's row whatever the defaults name and the row lacks, by verb name.
|
||||
func (i *Inventory) widenProtocol(ctx context.Context, s catalogue.Seat) error {
|
||||
var accepts, emits, serves []byte
|
||||
if err := i.store.Pool().QueryRow(ctx,
|
||||
`select accepts, emits, serves from seat where name = $1`, s.Name).Scan(&accepts, &emits, &serves); err != nil {
|
||||
return err
|
||||
}
|
||||
var row catalogue.Seat
|
||||
if err := json.Unmarshal(accepts, &row.Accepts); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(emits, &row.Emits); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(serves, &row.Serves); err != nil {
|
||||
return err
|
||||
}
|
||||
changed := false
|
||||
row.Accepts, changed = union(row.Accepts, s.Accepts, changed)
|
||||
row.Emits, changed = union(row.Emits, s.Emits, changed)
|
||||
have := map[string]bool{}
|
||||
for _, v := range row.Serves {
|
||||
have[v.Name] = true
|
||||
}
|
||||
for _, v := range s.Serves {
|
||||
if !have[v.Name] {
|
||||
row.Serves = append(row.Serves, v)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
a, e, sv, err := protocolJSON(row)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = i.store.Pool().Exec(ctx,
|
||||
`update seat set accepts = $2, emits = $3, serves = $4 where name = $1`, s.Name, a, e, sv)
|
||||
return err
|
||||
}
|
||||
|
||||
func union(have, want []string, changed bool) ([]string, bool) {
|
||||
seen := map[string]bool{}
|
||||
for _, h := range have {
|
||||
seen[h] = true
|
||||
}
|
||||
for _, w := range want {
|
||||
if !seen[w] {
|
||||
have = append(have, w)
|
||||
seen[w] = true
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return have, changed
|
||||
}
|
||||
|
||||
func protocolJSON(s catalogue.Seat) (accepts, emits, serves []byte, err error) {
|
||||
if accepts, err = json.Marshal(orEmpty(s.Accepts)); err != nil {
|
||||
return
|
||||
}
|
||||
if emits, err = json.Marshal(orEmpty(s.Emits)); err != nil {
|
||||
return
|
||||
}
|
||||
verbs := s.Serves
|
||||
if verbs == nil {
|
||||
verbs = []catalogue.Verb{}
|
||||
}
|
||||
serves, err = json.Marshal(verbs)
|
||||
return
|
||||
}
|
||||
|
||||
func orEmpty(s []string) []string {
|
||||
if s == nil {
|
||||
return []string{}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Aliases is every former seat name and the seat it now resolves to (novox/hq ADR 0122).
|
||||
func (i *Inventory) Aliases(ctx context.Context) (map[string]string, error) {
|
||||
rows, err := i.store.Pool().Query(ctx, `select alias, seat from seat_alias`)
|
||||
|
||||
Reference in New Issue
Block a user