Answer every placement candidate in shadow in the node's own key, applying nothing, so a placement change is checked by the nodes it places before it merges (issue 1126, ADR 0559 §4)
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"github.com/novox/mesh-host/internal/agent"
|
||||
@@ -317,4 +318,216 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) {
|
||||
if asked, _ := json.Marshal(sent["credentials"]); !strings.Contains(string(asked), `"role":"consumer"`) || !strings.Contains(string(asked), `"role":"provider"`) {
|
||||
t.Errorf("the vault was asked %s", asked)
|
||||
}
|
||||
// ADR 0559 §4: the node answers placement candidates in shadow, placed or not, applying nothing and handing no
|
||||
// composition: the same; a module the group adds, its secret not yet issued; a module removed; a placement it cannot
|
||||
// compose; a group whose keys went is no longer answered. What runs is never composed from a candidate: the
|
||||
// Composer does not read the state.
|
||||
for _, in := range New(env).inputs() {
|
||||
if in.bucket == delivery.PlacementCandidatesBucket {
|
||||
t.Fatal("the node's own composer reads placement candidates")
|
||||
}
|
||||
}
|
||||
sb := newPatternBus()
|
||||
trunk := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []delivery.PlacedModule{{Module: "lemurs"}}}
|
||||
mods := map[string]delivery.Module{"lemurs": {FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"},
|
||||
"keeper": {FP2: fp, Manifest: kdigest, Repository: "novox/k", Commit: "c"}}
|
||||
readOnly := &fakeBus{kv: map[string][]byte{delivery.ManifestsBucket + "/" + key: man, delivery.ManifestsBucket + "/" + kkey: kman}}
|
||||
senv := readOnly.env()
|
||||
senv.Watch = sb.watch
|
||||
senv.Own = func(context.Context, map[string]any) (json.RawMessage, error) {
|
||||
return json.RawMessage(`{"node":"n1","key":"sk","own":[]}`), nil // nothing issued for keeper: it is not placed here
|
||||
}
|
||||
// What n1 runs: lemurs and its own composition, as its composer composes them from the trunk.
|
||||
running, _, err := frombus.Compose(ctx, fixedReader{placements: map[string]delivery.Placement{"n1": trunk},
|
||||
records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}},
|
||||
modules: mods, manifests: map[string][]byte{digest: []byte(lemurs), kdigest: []byte(keeper)}}, frombus.Mesh{}, frombus.Scope{Node: "n1"}, map[string]string{"lemurs": fp})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
senv.Running = func() []delivery.Composed {
|
||||
var out []delivery.Composed
|
||||
for _, c := range running {
|
||||
out = append(out, c)
|
||||
}
|
||||
return out
|
||||
}
|
||||
answered := make(chan map[string]delivery.CandidateAnswer, 8)
|
||||
sh := NewShadow(senv, func(_ context.Context, a map[string]delivery.CandidateAnswer) { answered <- a })
|
||||
sctx, scancel := context.WithCancel(ctx)
|
||||
defer scancel()
|
||||
go func() { _ = sh.Run(sctx) }()
|
||||
sb.made(6)
|
||||
rec, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
|
||||
tp, _ := delivery.EncodePlacement(trunk)
|
||||
sb.put(delivery.PlacementBucket, "placement.n1", tp, 1)
|
||||
sb.put(delivery.HaveBucket, "n1.node", rec, 1)
|
||||
for name, m := range mods {
|
||||
raw, _ := delivery.EncodeModule(m)
|
||||
sb.put(delivery.ModulesBucket, name, raw, 1)
|
||||
}
|
||||
candidate := func(group string, rev uint64, modules ...string) {
|
||||
p := trunk
|
||||
p.Modules = nil
|
||||
for _, m := range modules {
|
||||
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
|
||||
}
|
||||
raw, err := delivery.EncodePlacementCandidate(delivery.PlacementCandidate{Group: group,
|
||||
Pull: delivery.Head{Repository: delivery.CatalogueRepository, Number: 1, Head: "h-" + group}, Placement: &p})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
k, _ := delivery.PlacementCandidateKey(group, "n1")
|
||||
sb.put(delivery.PlacementCandidatesBucket, k, raw, rev)
|
||||
}
|
||||
candidate("same", 3, "lemurs")
|
||||
candidate("adds", 4, "lemurs", "keeper")
|
||||
candidate("drops", 5)
|
||||
candidate("ghost", 6, "lemurs", "ghost")
|
||||
sb.caught()
|
||||
var answers map[string]delivery.CandidateAnswer
|
||||
waitFor := func(why string, ok func(map[string]delivery.CandidateAnswer) bool) {
|
||||
t.Helper()
|
||||
deadline := time.After(10 * time.Second)
|
||||
for answers == nil || !ok(answers) {
|
||||
select {
|
||||
case answers = <-answered:
|
||||
case <-deadline:
|
||||
t.Fatalf("%s: last answers %+v", why, answers)
|
||||
}
|
||||
}
|
||||
}
|
||||
waitFor("every group answered", func(a map[string]delivery.CandidateAnswer) bool { return len(a) == 4 })
|
||||
for group, want := range map[string]func(delivery.CandidateAnswer) bool{
|
||||
"same": func(a delivery.CandidateAnswer) bool {
|
||||
return a.Verdict == delivery.CandidateSame && a.Head == "h-same" && a.Revision == 3 && len(a.Same) == 2
|
||||
},
|
||||
"adds": func(a delivery.CandidateAnswer) bool {
|
||||
return a.Verdict == delivery.CandidateDiffers && len(a.Added) == 1 && a.Added[0].Module == "keeper" &&
|
||||
len(a.Added[0].Unissued) == 1 && len(a.Refusals) == 0
|
||||
},
|
||||
"drops": func(a delivery.CandidateAnswer) bool {
|
||||
return a.Verdict == delivery.CandidateDiffers && len(a.Removed) == 1 && a.Removed[0] == "lemurs"
|
||||
},
|
||||
"ghost": func(a delivery.CandidateAnswer) bool {
|
||||
return a.Verdict == delivery.CandidateRefused && a.Refused != ""
|
||||
},
|
||||
} {
|
||||
if !want(answers[group]) {
|
||||
t.Errorf("%s: %+v", group, answers[group])
|
||||
}
|
||||
}
|
||||
// The node's own key changing (its answers written) composes nothing again; a group's key deleted drops its answer.
|
||||
sb.put(delivery.HaveBucket, "n1.node", rec, 2)
|
||||
select {
|
||||
case a := <-answered:
|
||||
t.Errorf("answers handed again for nothing changed: %+v", a)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
k, _ := delivery.PlacementCandidateKey("same", "n1")
|
||||
sb.del(delivery.PlacementCandidatesBucket, k)
|
||||
waitFor("the deleted group's answer stays", func(a map[string]delivery.CandidateAnswer) bool { _, ok := a["same"]; return !ok && len(a) == 3 })
|
||||
}
|
||||
|
||||
// patternBus is a bus whose every watch gets the updates of its bucket matching its pattern, as the bus's own watches
|
||||
// do: two watches of one bucket each see what matches theirs.
|
||||
type patternBus struct {
|
||||
mu sync.Mutex
|
||||
watches []patternWatch
|
||||
}
|
||||
|
||||
type patternWatch struct {
|
||||
bucket, pattern string
|
||||
ch chan agent.Update
|
||||
}
|
||||
|
||||
func newPatternBus() *patternBus { return &patternBus{} }
|
||||
|
||||
func (b *patternBus) watch(_ context.Context, bucket, pattern string) (<-chan agent.Update, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
w := patternWatch{bucket: bucket, pattern: pattern, ch: make(chan agent.Update, 64)}
|
||||
b.watches = append(b.watches, w)
|
||||
return w.ch, nil
|
||||
}
|
||||
|
||||
func matches(pattern, key string) bool {
|
||||
p, k := strings.Split(pattern, "."), strings.Split(key, ".")
|
||||
for i, tok := range p {
|
||||
if tok == ">" {
|
||||
return len(k) > i
|
||||
}
|
||||
if i >= len(k) || (tok != "*" && tok != k[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(p) == len(k)
|
||||
}
|
||||
|
||||
func (b *patternBus) send(bucket string, u agent.Update) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for _, w := range b.watches {
|
||||
if w.bucket == bucket && (u.Caught || matches(w.pattern, u.Key)) {
|
||||
w.ch <- u
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *patternBus) put(bucket, key string, value []byte, rev uint64) {
|
||||
b.send(bucket, agent.Update{Key: key, Value: value, Revision: rev})
|
||||
}
|
||||
|
||||
func (b *patternBus) del(bucket, key string) { b.send(bucket, agent.Update{Key: key, Deleted: true}) }
|
||||
|
||||
// made waits until n watches are made, so nothing put is lost.
|
||||
func (b *patternBus) made(n int) {
|
||||
for {
|
||||
b.mu.Lock()
|
||||
got := len(b.watches)
|
||||
b.mu.Unlock()
|
||||
if got >= n {
|
||||
return
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// caught says every watch caught up.
|
||||
func (b *patternBus) caught() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for _, w := range b.watches {
|
||||
w.ch <- agent.Update{Caught: true}
|
||||
}
|
||||
}
|
||||
|
||||
// fixedReader is frombus's Reader over values held, for composing what a node runs.
|
||||
type fixedReader struct {
|
||||
placements map[string]delivery.Placement
|
||||
records map[string]delivery.NodeRecord
|
||||
modules map[string]delivery.Module
|
||||
manifests map[string][]byte
|
||||
}
|
||||
|
||||
func (r fixedReader) Placements(context.Context) (map[string]delivery.Placement, error) {
|
||||
return r.placements, nil
|
||||
}
|
||||
func (r fixedReader) Records(context.Context) (map[string]delivery.NodeRecord, error) {
|
||||
return r.records, nil
|
||||
}
|
||||
func (r fixedReader) Modules(context.Context) (map[string]delivery.Module, error) {
|
||||
return r.modules, nil
|
||||
}
|
||||
func (r fixedReader) Manifest(_ context.Context, d string) ([]byte, error) {
|
||||
if raw, ok := r.manifests[d]; ok {
|
||||
return raw, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no manifest %s", d)
|
||||
}
|
||||
func (r fixedReader) Values(context.Context, string, string) (map[string]any, error) { return nil, nil }
|
||||
func (r fixedReader) OwnSecrets(context.Context, string) ([]compose.OwnSecretRow, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r fixedReader) Credentials(context.Context, string, []compose.CredentialKey, []compose.CredentialKey) ([]compose.Credential, []compose.Issued, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user