minio: read who holds a seat from the placement seat's state, not the controller's seats (issue 1955, ADR 0571 clause 4)
This commit is contained in:
@@ -20,7 +20,7 @@ func manifest(t *testing.T) map[string]any {
|
||||
return m
|
||||
}
|
||||
|
||||
// 1516/AC6: minio claims object-store (scope mesh) serving link, invokes seat:mesh-controller.seats, builds the Go
|
||||
// 1516/AC6: minio claims object-store (scope mesh) serving link, reads the placement seat's seat-holders, builds the Go
|
||||
// bundle, and no tool named minio_presigned_url remains.
|
||||
func TestManifestClaimsTheSeat(t *testing.T) {
|
||||
m := manifest(t)
|
||||
@@ -38,9 +38,9 @@ func TestManifestClaimsTheSeat(t *testing.T) {
|
||||
if c["name"] != "object-store" || c["scope"] != "mesh" || len(serves) != 1 || serves[0] != "link" {
|
||||
t.Fatalf("claim: %v", c)
|
||||
}
|
||||
inv := m["invokes"].([]any)
|
||||
if len(inv) != 1 || inv[0] != "seat:mesh-controller.seats" {
|
||||
t.Fatalf("invokes: %v", inv)
|
||||
reads, _ := m["reads"].([]any)
|
||||
if _, invokes := m["invokes"]; invokes || len(reads) != 1 || reads[0] != "mesh-delivery-assigner.seat-holders" {
|
||||
t.Fatalf("reads %v and invokes %v: it reads the placement seat's holders and calls nothing", reads, m["invokes"])
|
||||
}
|
||||
var goBundle map[string]any
|
||||
for _, a := range m["build"].(map[string]any)["artifacts"].([]any) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/objectlink"
|
||||
)
|
||||
|
||||
@@ -59,11 +60,11 @@ func linkKeyID(bucket string) string {
|
||||
type server struct {
|
||||
configPath, linksDir, receives string
|
||||
now func() time.Time
|
||||
// seats asks the controller for its seats; a test replaces it.
|
||||
seats func() (json.RawMessage, error)
|
||||
// seats reads the placement seat's state of who holds which seat, every key; a test replaces it.
|
||||
seats func() (map[string][]byte, error)
|
||||
|
||||
mu sync.Mutex
|
||||
holders map[string]bool // "<seat>/<node>.<module>"
|
||||
keys map[string][]byte // seat-holders, as read
|
||||
readAt time.Time
|
||||
haveSet bool
|
||||
}
|
||||
@@ -77,8 +78,23 @@ func newServer(config, links, receives string) (*server, error) {
|
||||
return &server{configPath: config, linksDir: links, receives: receives, now: time.Now, seats: askSeats}, nil
|
||||
}
|
||||
|
||||
func askSeats() (json.RawMessage, error) {
|
||||
return stdio.Ask("seat:mesh-controller.seats", map[string]any{})
|
||||
func askSeats() (map[string][]byte, error) {
|
||||
state := stdio.State(delivery.SeatHoldersRead)
|
||||
names, err := state.Keys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := map[string][]byte{}
|
||||
for _, k := range names {
|
||||
e, err := state.Get(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e != nil {
|
||||
keys[k] = e.Value
|
||||
}
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// link answers one ask.
|
||||
@@ -206,7 +222,7 @@ func (s *server) allowed(g grant, node, module, method string) error {
|
||||
if seat, isSeat := strings.CutPrefix(sh.To, "seat:"); isSeat {
|
||||
held, err := s.holds(seat, node, module)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("a share names seat %s and the controller's seats cannot be read to tell whether this caller holds it: %w", seat, err)
|
||||
return false, fmt.Errorf("a share names seat %s and the placement seat's holders cannot be read to tell whether this caller holds it: %w", seat, err)
|
||||
}
|
||||
return held, nil
|
||||
}
|
||||
@@ -249,58 +265,26 @@ func has(list []string, v string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// holds says whether node.module holds the seat, from the controller's seats, read at most once a minute. A read that
|
||||
// failed is not cached: the next ask reads again.
|
||||
// holds says whether node.module holds the seat, from the placement seat's state of who holds which seat (novox/hq ADR
|
||||
// 0571 clause 4), read at most once a minute. A read that failed is not cached: the next ask reads again.
|
||||
func (s *server) holds(seat, node, module string) (bool, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if !s.haveSet || s.now().Sub(s.readAt) >= holdersFor {
|
||||
raw, err := s.seats()
|
||||
keys, err := s.seats()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
set, err := holdersIn(raw)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
s.holders, s.readAt, s.haveSet = set, s.now(), true
|
||||
s.keys, s.readAt, s.haveSet = keys, s.now(), true
|
||||
}
|
||||
return s.holders[seat+"/"+node+"."+module], nil
|
||||
}
|
||||
|
||||
// holdersIn reads the controller's `seats` answer (answer.seats[].holders[] of {node, module}, and answer.outside[] of
|
||||
// {Claim, Node, Module}) into "<seat>/<node>.<module>". The answer may arrive wrapped in {"answer": ...}.
|
||||
func holdersIn(raw json.RawMessage) (map[string]bool, error) {
|
||||
var doc struct {
|
||||
Answer json.RawMessage `json:"answer"`
|
||||
Seats []struct {
|
||||
Seat string `json:"seat"`
|
||||
Holders []struct {
|
||||
Node string `json:"node"`
|
||||
Module string `json:"module"`
|
||||
} `json:"holders"`
|
||||
} `json:"seats"`
|
||||
Outside []struct {
|
||||
Claim, Node, Module string
|
||||
} `json:"outside"`
|
||||
holders, err := delivery.HoldersOf(s.keys, seat, node)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return nil, fmt.Errorf("the controller's seats answer is not JSON: %w", err)
|
||||
}
|
||||
if len(doc.Answer) > 0 && string(doc.Answer) != "null" {
|
||||
return holdersIn(doc.Answer)
|
||||
}
|
||||
if doc.Seats == nil && doc.Outside == nil {
|
||||
return nil, errors.New("the controller's seats answer holds neither seats nor outside claims")
|
||||
}
|
||||
out := map[string]bool{}
|
||||
for _, s := range doc.Seats {
|
||||
for _, h := range s.Holders {
|
||||
out[s.Seat+"/"+h.Node+"."+h.Module] = true
|
||||
for _, h := range holders {
|
||||
if h.Node == node && h.Module == module {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
for _, o := range doc.Outside {
|
||||
out[o.Claim+"/"+o.Node+"."+o.Module] = true
|
||||
}
|
||||
return out, nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -25,12 +24,13 @@ const grantFile = `{
|
||||
]
|
||||
}`
|
||||
|
||||
// The seats answer is the controller's (mesh-controller.seats read on 2026-10-21): seats[].holders[] of {node, module},
|
||||
// and outside[] of {Claim, Node, Module}.
|
||||
const seatsAnswer = `{"answer": {"seats": [
|
||||
{"seat": "node-files", "scope": "node", "holders": [{"node": "g14", "module": "mesh-files"}, {"node": "shanks", "module": "mesh-files"}]},
|
||||
{"seat": "node-backup", "scope": "node", "holders": [{"node": "g14", "module": "restic"}]}
|
||||
], "outside": [{"Claim": "printer", "Scope": "mesh", "Node": "shanks", "Module": "brother", "Site": "home", "Kind": ""}]}}`
|
||||
// The placement seat's state of who holds which seat (seat-holders): a key per node seat and node, a key per mesh seat.
|
||||
var seatHolderKeys = map[string][]byte{
|
||||
"node-files.g14": []byte(`{"v":1,"seat":"node-files","scope":"node","node":"g14","holders":[{"module":"mesh-files","node":"g14"}]}`),
|
||||
"node-files.shanks": []byte(`{"v":1,"seat":"node-files","scope":"node","node":"shanks","holders":[{"module":"mesh-files","node":"shanks"}]}`),
|
||||
"node-backup.g14": []byte(`{"v":1,"seat":"node-backup","scope":"node","node":"g14","holders":[{"module":"restic","node":"g14"}]}`),
|
||||
"printer": []byte(`{"v":1,"seat":"printer","scope":"mesh","holders":[{"module":"brother","node":"shanks"}]}`),
|
||||
}
|
||||
|
||||
type rig struct {
|
||||
s *server
|
||||
@@ -64,12 +64,12 @@ func newRig(t *testing.T) *rig {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.now = func() time.Time { return r.now }
|
||||
s.seats = func() (json.RawMessage, error) {
|
||||
s.seats = func() (map[string][]byte, error) {
|
||||
r.seatsCalls++
|
||||
if r.seatsErr != nil {
|
||||
return nil, r.seatsErr
|
||||
}
|
||||
return json.RawMessage(seatsAnswer), nil
|
||||
return seatHolderKeys, nil
|
||||
}
|
||||
r.s = s
|
||||
return r
|
||||
|
||||
@@ -2,4 +2,4 @@ module minio
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122348-6c857e1c025f
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029220947-f9a496d3e121
|
||||
|
||||
+14
-2
@@ -1,2 +1,14 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122348-6c857e1c025f h1:7J/9jOfSfjfYpf8MOj96HwQRViQUkBNMb71RHP2mjn8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122348-6c857e1c025f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029220947-f9a496d3e121 h1:OsA1v6n736a7RqEV8Nkip8pf5ILMbzj0PWuKqcBOhK0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029220947-f9a496d3e121/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"invokes": [
|
||||
"seat:mesh-controller.seats"
|
||||
"reads": [
|
||||
"mesh-delivery-assigner.seat-holders"
|
||||
],
|
||||
"contributes": {
|
||||
"route": {
|
||||
|
||||
Reference in New Issue
Block a user