stage 2 of 8 for issue 1389

This commit is contained in:
2026-10-20 11:10:35 +00:00
parent f70b0c5077
commit df351458aa
18 changed files with 51 additions and 171 deletions
+11 -23
View File
@@ -8,7 +8,6 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose" "git.novox.be/novox/mesh-sdk/go/compose"
"github.com/novox/mesh-controller/internal/inventory"
) )
// A declaration as the SDK composes it is the one the controller sends, byte for byte, whatever it carries: the body is the // A declaration as the SDK composes it is the one the controller sends, byte for byte, whatever it carries: the body is the
@@ -38,39 +37,28 @@ func TestASendableIsTheComposedDeclarationByteForByte(t *testing.T) {
} }
} }
// 1386: what a send records as carried is what the controller's own carriedOn recorded. // 1386: what a send records as carried: the current build of each module it carries, the build a module left out of it was last
// sent (when that is known), and a module kept at the build the machine runs at that one. (The controller's carriedOn recorded
// the same; mesh-sdk's node_test TestAModuleLeftOutKeepsTheBuildItWasLastSent holds the left-out case.)
func TestWhatASendRecordsAsCarriedIsWhatTheControllerRecorded(t *testing.T) { func TestWhatASendRecordsAsCarriedIsWhatTheControllerRecorded(t *testing.T) {
plan := catalogue.Resolution{Modules: []catalogue.Manifest{{Module: "postgres"}, {Module: "files"}, {Module: "cache"}}} plan := catalogue.Resolution{Modules: []catalogue.Manifest{{Module: "postgres"}, {Module: "files"}, {Module: "cache"}}}
current := map[string]inventory.CurrentBuild{"postgres": {Commit: "p2"}, "files": {Commit: "f2"}, "cache": {Commit: "c2"}}
leftOut := map[string]string{"cache": "a setting cannot compose"} leftOut := map[string]string{"cache": "a setting cannot compose"}
for name, c := range map[string]struct { for name, c := range map[string]struct {
sent map[string]string sent map[string]string
known bool known bool
kept map[string]string kept map[string]string
want map[string]string
}{ }{
"nothing sent": {}, "nothing sent": {want: map[string]string{"postgres": "p2", "files": "f2"}},
"sent and kept": {sent: map[string]string{"postgres": "p1", "cache": "c1", "files": "f1"}, known: true, kept: map[string]string{"postgres": "p1"}}, "sent and kept": {sent: map[string]string{"postgres": "p1", "cache": "c1", "files": "f1"}, known: true,
"sent but not known": {sent: map[string]string{"cache": "c1"}}, kept: map[string]string{"postgres": "p1"}, want: map[string]string{"postgres": "p1", "files": "f2", "cache": "c1"}},
"kept a module not in": {known: true, kept: map[string]string{"mail": "m1"}}, "sent but not known": {sent: map[string]string{"cache": "c1"}, want: map[string]string{"postgres": "p2", "files": "f2"}},
"kept a module not in": {known: true, kept: map[string]string{"mail": "m1"}, want: map[string]string{"postgres": "p2", "files": "f2"}},
} { } {
var names []string
for _, m := range plan.Modules {
names = append(names, m.Module)
}
before := c.sent
if !c.known {
before = nil
}
want := carriedBuilds(names, leftOut, current, before)
for m, was := range c.kept {
if _, carried := want[m]; carried {
want[m] = was
}
}
in := compose.Input{CurrentBuilds: map[string]string{"postgres": "p2", "files": "f2", "cache": "c2"}} in := compose.Input{CurrentBuilds: map[string]string{"postgres": "p2", "files": "f2", "cache": "c2"}}
node := compose.Node{SentBuilds: c.sent, SentKnown: c.known, KeptBuilds: c.kept} node := compose.Node{SentBuilds: c.sent, SentKnown: c.known, KeptBuilds: c.kept}
if got := buildsOf(in, node, plan, leftOut); !reflect.DeepEqual(got, want) { if got := buildsOf(in, node, plan, leftOut); !reflect.DeepEqual(got, c.want) {
t.Errorf("%s: a send records %v, the controller recorded %v", name, got, want) t.Errorf("%s: a send records %v, want %v", name, got, c.want)
} }
} }
} }
+2 -6
View File
@@ -127,21 +127,17 @@ type snapshotFacts = snapshot.Facts
func toldEveryMachine(t *testing.T, open *stores, served map[string]map[string]any, on map[string][]string) map[string]string { func toldEveryMachine(t *testing.T, open *stores, served map[string]map[string]any, on map[string][]string) map[string]string {
t.Helper() t.Helper()
ctx := t.Context() ctx := t.Context()
gens, err := generators(ctx, open)
if err != nil {
t.Fatal(err)
}
shelf, err := open.inventory.Catalogue(ctx) shelf, err := open.inventory.Catalogue(ctx)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
told := map[string]string{} told := map[string]string{}
for _, node := range fourMachines { for _, node := range fourMachines {
plan, settings, err := planFor(ctx, open, node) plan, _, err := resolveNode(ctx, open, node)
if err != nil { if err != nil {
t.Fatalf("%s: %v", node, err) t.Fatalf("%s: %v", node, err)
} }
with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Allocating) with, err := madeAndRendered(ctx, open, node)
if err != nil { if err != nil {
t.Fatalf("%s: %v", node, err) t.Fatalf("%s: %v", node, err)
} }
+7 -17
View File
@@ -16,7 +16,6 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
) )
// The core's own credentials, sealed by the vault (novox/hq ADR 0349 §16, issue 755; the lead's decision on issue 755, // The core's own credentials, sealed by the vault (novox/hq ADR 0349 §16, issue 755; the lead's decision on issue 755,
@@ -40,15 +39,6 @@ import (
// the vault and the machine, cleared by the first send the vault answers whole. Only a machine with no sealing key, or // the vault and the machine, cleared by the first send the vault answers whole. Only a machine with no sealing key, or
// one whose key cannot be read, leaves its module out. // one whose key cannot be read, leaves its module out.
// engineOpensVaultBox says whether a machine's node-engine opens the vault's box, by its own newest statement.
func engineOpensVaultBox(ctx context.Context, inv *inventory.Inventory, node string) (bool, error) {
stated, had, err := inv.HealthOf(ctx, node)
if err != nil {
return false, err
}
return had && stated.Contract >= link.VaultBoxContract, nil
}
// coreAnswer is the vault's `get` answer as the controller reads it: the name, the generation and the box, never // coreAnswer is the vault's `get` answer as the controller reads it: the name, the generation and the box, never
// opened. // opened.
type coreAnswer struct { type coreAnswer struct {
@@ -121,7 +111,7 @@ func sealingKeyPEM(raw string) (string, error) {
// own seal where nothing was kept; on any other question, the kept box, or a stand-in the next send asks for. Pure: what to keep and what to // own seal where nothing was kept; on any other question, the kept box, or a stand-in the next send asks for. Pure: what to keep and what to
// say is answered, not done. // say is answered, not done.
func coreBoxesOf(node, sealingKey string, core map[string][]string, kept map[string]map[string]inventory.CoreBox, func coreBoxesOf(node, sealingKey string, core map[string][]string, kept map[string]map[string]inventory.CoreBox,
choosing Choosing, ask coreAsk) coreBoxes { allocating bool, ask coreAsk) coreBoxes {
out := coreBoxes{sealed: map[string]map[string]json.RawMessage{}, notPlaced: map[string]string{}} out := coreBoxes{sealed: map[string]map[string]json.RawMessage{}, notPlaced: map[string]string{}}
modules := make([]string, 0, len(core)) modules := make([]string, 0, len(core))
for m := range core { for m := range core {
@@ -143,7 +133,7 @@ func coreBoxesOf(node, sealingKey string, core map[string][]string, kept map[str
for _, name := range core[module] { for _, name := range core[module] {
k, has := kept[module][name] k, has := kept[module][name]
valid := has && k.SealedTo == sealingKey && len(k.Box) > 0 valid := has && k.SealedTo == sealingKey && len(k.Box) > 0
if choosing != Allocating { if !allocating {
if valid { if valid {
boxes[name] = k.Box boxes[name] = k.Box
} else { } else {
@@ -218,10 +208,10 @@ var coreVault = func(ctx context.Context) coreAsk {
// coreBoxesFor is coreBoxesOf for one machine, read from and kept in the store, and said on a send. // coreBoxesFor is coreBoxesOf for one machine, read from and kept in the store, and said on a send.
func coreBoxesFor(ctx context.Context, inv *inventory.Inventory, node string, modules []catalogue.Manifest, func coreBoxesFor(ctx context.Context, inv *inventory.Inventory, node string, modules []catalogue.Manifest,
handed func(catalogue.Manifest) bool, choosing Choosing) (coreBoxes, error) { handed func(catalogue.Manifest) bool, allocating bool) (coreBoxes, error) {
core := coreModulesOf(modules, handed) core := coreModulesOf(modules, handed)
if len(core) == 0 { if len(core) == 0 {
if choosing == Allocating { if allocating {
settleVaultSilent(ctx, node) settleVaultSilent(ctx, node)
} }
return coreBoxes{}, nil return coreBoxes{}, nil
@@ -235,16 +225,16 @@ func coreBoxesFor(ctx context.Context, inv *inventory.Inventory, node string, mo
return coreBoxes{}, err return coreBoxes{}, err
} }
var ask coreAsk var ask coreAsk
if choosing == Allocating { if allocating {
ask = coreVault(ctx) ask = coreVault(ctx)
} }
out := coreBoxesOf(node, key, core, kept, choosing, ask) out := coreBoxesOf(node, key, core, kept, allocating, ask)
for _, b := range out.keep { for _, b := range out.keep {
if err := inv.KeepCoreBox(ctx, node, b); err != nil { if err := inv.KeepCoreBox(ctx, node, b); err != nil {
return coreBoxes{}, err return coreBoxes{}, err
} }
} }
if choosing == Allocating { if allocating {
sayVaultSilent(ctx, node, out) sayVaultSilent(ctx, node, out)
} }
return out, nil return out, nil
@@ -96,7 +96,7 @@ func TestASilentVaultHoldsTheKeptBoxAndSaysSo(t *testing.T) {
func TestACoreModuleFirstPlacedWhileTheVaultIsSilentIsSealedByTheController(t *testing.T) { func TestACoreModuleFirstPlacedWhileTheVaultIsSilentIsSealedByTheController(t *testing.T) {
open, vault := aCoreMesh(t, link.VaultBoxContract) open, vault := aCoreMesh(t, link.VaultBoxContract)
vault.silent = errors.New("no responders") vault.silent = errors.New("no responders")
plan, settings, err := planFor(t.Context(), open, "laptop") plan, settings, err := resolveNode(t.Context(), open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+6 -6
View File
@@ -68,7 +68,7 @@ func TestReplay908(t *testing.T) {
key := "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" key := "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
core := map[string][]string{"nats": {"password"}} core := map[string][]string{"nats": {"password"}}
ask := func(name, publicKey string) (coreAnswer, error) { return coreAnswer{}, errors.New("no responders") } ask := func(name, publicKey string) (coreAnswer, error) { return coreAnswer{}, errors.New("no responders") }
got := coreBoxesOf("novox", key, core, nil, Allocating, ask) got := coreBoxesOf("novox", key, core, nil, true, ask)
if _, sealed := got.sealed["nats"]; sealed || len(got.notPlaced) != 0 { if _, sealed := got.sealed["nats"]; sealed || len(got.notPlaced) != 0 {
t.Fatalf("nats is vault-sealed %v or not placed %v", got.sealed, got.notPlaced) t.Fatalf("nats is vault-sealed %v or not placed %v", got.sealed, got.notPlaced)
} }
@@ -81,7 +81,7 @@ func TestReplay908(t *testing.T) {
} }
// A kept box is still carried while the vault is silent. // A kept box is still carried while the vault is silent.
kept := map[string]map[string]inventory.CoreBox{"nats": {"password": {SealedTo: key, Box: json.RawMessage(`{"v":1}`)}}} kept := map[string]map[string]inventory.CoreBox{"nats": {"password": {SealedTo: key, Box: json.RawMessage(`{"v":1}`)}}}
got = coreBoxesOf("novox", key, core, kept, Allocating, ask) got = coreBoxesOf("novox", key, core, kept, true, ask)
if string(got.sealed["nats"]["password"]) != `{"v":1}` || len(got.byController) != 0 { if string(got.sealed["nats"]["password"]) != `{"v":1}` || len(got.byController) != 0 {
t.Fatalf("the kept box was not held: %+v", got) t.Fatalf("the kept box was not held: %+v", got)
} }
@@ -99,17 +99,17 @@ func TestOnlyASendAsksTheVault(t *testing.T) {
t.Fatal("the vault was asked by a question that is not a send") t.Fatal("the vault was asked by a question that is not a send")
return coreAnswer{}, nil return coreAnswer{}, nil
} }
for _, choosing := range []Choosing{Reading, Foreseeing} { for _, allocating := range []bool{false} {
if got := coreBoxesOf("anchor", key, core, kept, choosing, never); string(got.sealed["nats"]["broker"]) != `{"v":1}` { if got := coreBoxesOf("anchor", key, core, kept, allocating, never); string(got.sealed["nats"]["broker"]) != `{"v":1}` {
t.Errorf("read %s", got.sealed["nats"]["broker"]) t.Errorf("read %s", got.sealed["nats"]["broker"])
} }
if got := coreBoxesOf("anchor", key, core, nil, choosing, never); string(got.sealed["nats"]["broker"]) != if got := coreBoxesOf("anchor", key, core, nil, allocating, never); string(got.sealed["nats"]["broker"]) !=
string(catalogue.ForeseenVaultBox) { string(catalogue.ForeseenVaultBox) {
t.Errorf("with none kept, read %s", got.sealed["nats"]["broker"]) t.Errorf("with none kept, read %s", got.sealed["nats"]["broker"])
} }
} }
// A box kept for another key of the machine is never carried. // A box kept for another key of the machine is never carried.
other := coreBoxesOf("anchor", aPublicKey(t), core, kept, Allocating, other := coreBoxesOf("anchor", aPublicKey(t), core, kept, true,
func(string, string) (coreAnswer, error) { return coreAnswer{}, errors.New("silent") }) func(string, string) (coreAnswer, error) { return coreAnswer{}, errors.New("silent") })
if _, vaultSealed := other.sealed["nats"]; vaultSealed || !reflect.DeepEqual(other.byController, []string{"nats"}) { if _, vaultSealed := other.sealed["nats"]; vaultSealed || !reflect.DeepEqual(other.byController, []string{"nats"}) {
t.Fatalf("a box sealed to the machine's old key was carried: %+v", other) t.Fatalf("a box sealed to the machine's old key was carried: %+v", other)
@@ -40,7 +40,7 @@ func TestAssigningAModuleThatSpeaksOnTheBusNamesItsCredential(t *testing.T) {
} }
// And the push refuses to send it, naming the same verb, rather than sealing a placeholder. // And the push refuses to send it, naming the same verb, rather than sealing a placeholder.
plan, settings, err := planFor(ctx, open, "laptop") plan, settings, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+2 -10
View File
@@ -42,12 +42,8 @@ func TestAMachineIsSentTheEpochOnlyOnceItSaysItReadsOne(t *testing.T) {
if err := inv.RecordReadsEpoch(ctx, anchor.ID, true); err != nil { if err := inv.RecordReadsEpoch(ctx, anchor.ID, true); err != nil {
t.Fatal(err) t.Fatal(err)
} }
gens, err := generators(ctx, open)
if err != nil {
t.Fatal(err)
}
d := &bodiesDelivery{recordedDelivery: recordedDelivery{inv: inv}, bodies: map[string][]byte{}} d := &bodiesDelivery{recordedDelivery: recordedDelivery{inv: inv}, bodies: map[string][]byte{}}
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, composeForPush(open, gens), d, "", nil); err != nil { if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, composeForPush(open), d, "", nil); err != nil {
t.Fatal(err) t.Fatal(err)
} }
carried := func(node string) (epoch float64, has bool) { carried := func(node string) (epoch float64, has bool) {
@@ -89,12 +85,8 @@ func TestNothingIsComposedOrSentWithoutTheLease(t *testing.T) {
was := epochForActs was := epochForActs
epochForActs = func(context.Context) (uint64, error) { return 0, errors.New("this controller lost the lease") } epochForActs = func(context.Context) (uint64, error) { return 0, errors.New("this controller lost the lease") }
t.Cleanup(func() { epochForActs = was }) t.Cleanup(func() { epochForActs = was })
gens, err := generators(ctx, open)
if err != nil {
t.Fatal(err)
}
d := &bodiesDelivery{recordedDelivery: recordedDelivery{inv: open.inventory}, bodies: map[string][]byte{}} d := &bodiesDelivery{recordedDelivery: recordedDelivery{inv: open.inventory}, bodies: map[string][]byte{}}
refused, err := sendRound(ctx, open, []string{"anchor"}, composeForPush(open, gens), d, "", nil) refused, err := sendRound(ctx, open, []string{"anchor"}, composeForPush(open), d, "", nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
-37
View File
@@ -14,8 +14,6 @@ import (
"sync" "sync"
"time" "time"
"git.novox.be/novox/mesh-sdk/go/validate"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose" "git.novox.be/novox/mesh-sdk/go/compose"
snapshot "git.novox.be/novox/mesh-sdk/go/facts" snapshot "git.novox.be/novox/mesh-sdk/go/facts"
@@ -504,41 +502,6 @@ func declarationFacts(ctx context.Context, open *stores, node string, gensErr er
return d return d
} }
// composedAndValidated composes one machine's declaration — as a push would (Allocating) or as the next
// push will without making anything (Foreseeing) — with the order it was last sent, and runs the
// node-engine's own validator over the body. An error is that it did not compose; problems are what the
// validator refuses.
func composedAndValidated(ctx context.Context, open *stores, node string, gens map[string]catalogue.Generator,
choosing Choosing) (sendable, []string, error) {
plan, settings, err := planFor(ctx, open, node)
if err != nil {
return sendable{}, nil, err
}
declared, err := declarationWith(ctx, open, node, plan, settings, gens, choosing)
if err != nil {
return sendable{}, nil, err
}
record, err := open.inventory.NodeByName(ctx, node)
if err != nil {
return sendable{}, nil, err
}
if declared.Sequence, err = open.inventory.Sequence(ctx, record.ID); err != nil {
return sendable{}, nil, err
}
if declared.Epoch, err = open.inventory.SentEpoch(ctx, record.ID); err != nil {
return sendable{}, nil, err
}
// And the generation it was last sent, as the would-send is (novox/hq issue 234).
if declared.Generation, err = open.inventory.SentGeneration(ctx, record.ID); err != nil {
return sendable{}, nil, err
}
body, err := declared.Body()
if err != nil {
return sendable{}, nil, err
}
return declared, validate.Declaration(body), nil
}
// resourceNames are a declaration's resources as `type:id`, sorted. // resourceNames are a declaration's resources as `type:id`, sorted.
func resourceNames(resources []map[string]any) []string { func resourceNames(resources []map[string]any) []string {
out := make([]string, 0, len(resources)) out := make([]string, 0, len(resources))
+1 -5
View File
@@ -42,12 +42,8 @@ func aMeshWithSecrets(t *testing.T) (*stores, []string) {
t.Fatal(err) t.Fatal(err)
} }
// A push's worth of composition, which makes the pair credential the consumer is sent. // A push's worth of composition, which makes the pair credential the consumer is sent.
gens, err := generators(ctx, open)
if err != nil {
t.Fatal(err)
}
for _, node := range []string{"laptop", "anchor"} { for _, node := range []string{"laptop", "anchor"} {
if _, _, err := composedAndValidated(ctx, open, node, gens, Allocating); err != nil { if _, err := composeSend(ctx, open, node); err != nil {
t.Fatalf("%s does not compose: %v", node, err) t.Fatalf("%s does not compose: %v", node, err)
} }
} }
-13
View File
@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"sort"
"strings" "strings"
"time" "time"
@@ -103,18 +102,6 @@ func senderOf(caller string) string {
return fmt.Sprintf("%s (pid %d on %s, build %s)", caller, os.Getpid(), host, version) return fmt.Sprintf("%s (pid %d on %s, build %s)", caller, os.Getpid(), host, version)
} }
// namedModules are the modules a declaration names: its machine's set, less what was left out of it.
func namedModules(plan catalogue.Resolution, leftOut map[string]string) []string {
out := make([]string, 0, len(plan.Modules))
for _, m := range plan.Modules {
if _, left := leftOut[m.Module]; !left {
out = append(out, m.Module)
}
}
sort.Strings(out)
return out
}
// heardGenerationRefusal keeps a machine's refusal of a send for its generation, so S20 names its sender // heardGenerationRefusal keeps a machine's refusal of a send for its generation, so S20 names its sender
// and says it (novox/hq issue 234): on the send it refused or — when the mesh has no record of sending that // and says it (novox/hq issue 234): on the send it refused or — when the mesh has no record of sending that
// sequence — as a refusal of its own, naming the sender as unknown. Either way S20 is raised: a refusal the // sequence — as a refusal of its own, naming the sender as unknown. Either way S20 is raised: a refusal the
@@ -35,7 +35,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
if err := inv.PinProvision(ctx, "laptop", "postgres-database", "anchor", "store"); err != nil { if err := inv.PinProvision(ctx, "laptop", "postgres-database", "anchor", "store"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
plan, _, err := planFor(ctx, open, "laptop") plan, _, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -45,7 +45,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
if err := inv.RecordBindings(ctx, "laptop", boundToData(plan, nil)); err != nil { if err := inv.RecordBindings(ctx, "laptop", boundToData(plan, nil)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
grants, _, unbound, err := grantsFor(ctx, open, "anchor") grants, _, unbound, err := grantsOf(ctx, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -57,7 +57,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
if err := inv.PinProvision(ctx, "laptop", "postgres-database", "laptop", "store"); err != nil { if err := inv.PinProvision(ctx, "laptop", "postgres-database", "laptop", "store"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
plan, _, err = planFor(ctx, open, "laptop") plan, _, err = resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -76,7 +76,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
} }
// The store it left: no longer granted, and said. // The store it left: no longer granted, and said.
grants, _, unbound, err = grantsFor(ctx, open, "anchor") grants, _, unbound, err = grantsOf(ctx, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -87,7 +87,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
strings.Join(unbound[0].BoundTo, ",") != "laptop" { strings.Join(unbound[0].BoundTo, ",") != "laptop" {
t.Fatalf("the consumer that moved is not the one said: %+v", unbound) t.Fatalf("the consumer that moved is not the one said: %+v", unbound)
} }
planned, settings, err := planFor(ctx, open, "anchor") planned, settings, err := resolveNode(ctx, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -105,7 +105,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
} }
// The store it is bound to: granted. // The store it is bound to: granted.
grants, _, unbound, err = grantsFor(ctx, open, "laptop") grants, _, unbound, err = grantsOf(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -131,7 +131,7 @@ func TestAConsumerWhoseResolutionCannotBeReadIsNotWithdrawn(t *testing.T) {
t.Fatalf("assign %s %s: %v", a[0], a[1], err) t.Fatalf("assign %s %s: %v", a[0], a[1], err)
} }
} }
if _, _, err := planFor(ctx, open, "laptop"); err != nil { if _, _, err := resolveNode(ctx, open, "laptop"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -144,10 +144,10 @@ func TestAConsumerWhoseResolutionCannotBeReadIsNotWithdrawn(t *testing.T) {
if err := inv.RecordSealingKey(ctx, laptop.ID, "not a key"); err != nil { if err := inv.RecordSealingKey(ctx, laptop.ID, "not a key"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if _, _, err := planFor(ctx, open, "laptop"); err == nil || unresolvable(err) { if _, _, err := resolveNode(ctx, open, "laptop"); err == nil || unresolvable(err) {
t.Fatalf("the seam this test relies on moved: %v", err) t.Fatalf("the seam this test relies on moved: %v", err)
} }
grants, _, unbound, err := grantsFor(ctx, open, "anchor") grants, _, unbound, err := grantsOf(ctx, open, "anchor")
if err == nil { if err == nil {
t.Fatalf("an unreadable consumer was answered: grants %+v, unbound %+v", grants, unbound) t.Fatalf("an unreadable consumer was answered: grants %+v, unbound %+v", grants, unbound)
} }
@@ -66,15 +66,11 @@ func TestTheGrantsStepSendsTheUserListAndNothingElse(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
plan, settings, err := planFor(under, open, "anchor") plan, _, err := resolveNode(under, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
gens, err := generators(under, open) declared, err := composeSend(under, open, "anchor")
if err != nil {
t.Fatal(err)
}
declared, err := declarationWith(under, open, "anchor", plan, settings, gens, Allocating)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+2 -5
View File
@@ -7,7 +7,6 @@ import (
"sort" "sort"
"strings" "strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
) )
@@ -265,11 +264,9 @@ func flushBehind(ctx context.Context, open *stores, nodes []inventory.Node, hand
// composeForPush is how a push composes one machine: its set resolved, what it cannot host and what // composeForPush is how a push composes one machine: its set resolved, what it cannot host and what
// is left out of it said, and its declaration allocated. // is left out of it said, and its declaration allocated.
func composeForPush(open *stores, gens map[string]catalogue.Generator) func(held context.Context, node string) (sendable, error) { func composeForPush(open *stores) func(held context.Context, node string) (sendable, error) {
return func(held context.Context, node string) (sendable, error) { return func(held context.Context, node string) (sendable, error) {
// Read afresh for each machine: the send before it made and recorded things. gens is the private network the // Read afresh for each machine: the send before it made and recorded things.
// controller's own composition took; the SDK's is computed from the same Input.
_ = gens
held = freshInput(held) held = freshInput(held)
plan, _, err := resolveNode(held, open, node) plan, _, err := resolveNode(held, open, node)
if err != nil { if err != nil {
@@ -28,11 +28,7 @@ func TestANamedPushLeavesAMachineAPolicyHoldsBack(t *testing.T) {
if err := inv.SetUpgradeOf(ctx, "resolver", inventory.Upgrade{Why: "each machine checked by hand"}); err != nil { if err := inv.SetUpgradeOf(ctx, "resolver", inventory.Upgrade{Why: "each machine checked by hand"}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
gens, err := generators(ctx, open) compose := composeForPush(open)
if err != nil {
t.Fatal(err)
}
compose := composeForPush(open, gens)
d := &recordedDelivery{inv: inv} d := &recordedDelivery{inv: inv}
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil { if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -134,11 +130,7 @@ func TestANamedPushStillSendsAConsequenceNothingHolds(t *testing.T) {
if _, err := inv.Assign(ctx, "laptop", "resolver"); err != nil { if _, err := inv.Assign(ctx, "laptop", "resolver"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
gens, err := generators(ctx, open) compose := composeForPush(open)
if err != nil {
t.Fatal(err)
}
compose := composeForPush(open, gens)
d := &recordedDelivery{inv: inv} d := &recordedDelivery{inv: inv}
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil { if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil {
t.Fatal(err) t.Fatal(err)
-17
View File
@@ -3,7 +3,6 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -99,22 +98,6 @@ func TestAPlanWaitingOnItsFirstMachineHoldsTheRest(t *testing.T) {
} }
} }
// A send records the build of each module it carried; a module left out of it keeps the build it
// was last sent, since the machine keeps that one.
func TestASendCarriesTheCurrentBuildsAndALeftOutModuleKeepsItsOwn(t *testing.T) {
current := map[string]inventory.CurrentBuild{"a": {Commit: "a2"}, "b": {Commit: "b2"}, "c": {}}
got := carriedBuilds([]string{"a", "b", "c"}, map[string]string{"b": "a setting does not compose"},
current, map[string]string{"a": "a1", "b": "b1"})
if want := map[string]string{"a": "a2", "b": "b1", "c": ""}; !reflect.DeepEqual(got, want) {
t.Fatalf("carried %v, wanted %v", got, want)
}
// Not known before: the left-out module is not recorded at all, so it reads as never sent.
got = carriedBuilds([]string{"a", "b"}, map[string]string{"b": "x"}, current, nil)
if want := map[string]string{"a": "a2"}; !reflect.DeepEqual(got, want) {
t.Fatalf("carried %v, wanted %v", got, want)
}
}
// recordedDelivery sends nothing and records each send as the mesh does, so the next comparison // recordedDelivery sends nothing and records each send as the mesh does, so the next comparison
// reads the machine as current — and writes down which machines it declared. // reads the machine as current — and writes down which machines it declared.
type recordedDelivery struct { type recordedDelivery struct {
+1 -1
View File
@@ -63,7 +63,7 @@ func TestASecondEligibleHolderStandsBesideTheOneHeldByBeingAlone(t *testing.T) {
// And the record decides from here: the anchor's plan holds the seat, the laptop's does not. // And the record decides from here: the anchor's plan holds the seat, the laptop's does not.
for node, holds := range map[string]bool{"anchor": true, "laptop": false} { for node, holds := range map[string]bool{"anchor": true, "laptop": false} {
plan, _, err := planFor(ctx, open, node) plan, _, err := resolveNode(ctx, open, node)
if err != nil { if err != nil {
t.Fatalf("%s no longer resolves: %v", node, err) t.Fatalf("%s no longer resolves: %v", node, err)
} }
@@ -31,7 +31,7 @@ func TestAnOverflowingConsumerNeverRefusesItsProvidersMachine(t *testing.T) {
} }
// The consumer's machine resolves, and says which of its modules no provider will grant. // The consumer's machine resolves, and says which of its modules no provider will grant.
consumer, _, err := planFor(ctx, open, "laptop") consumer, _, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -42,7 +42,7 @@ func TestAnOverflowingConsumerNeverRefusesItsProvidersMachine(t *testing.T) {
// The provider's machine composes. Under ADR 0049's one bound this was a refusal naming // The provider's machine composes. Under ADR 0049's one bound this was a refusal naming
// networkmanager, and no push to the provider could go through. // networkmanager, and no push to the provider could go through.
plan, settings, err := planFor(ctx, open, "anchor") plan, settings, err := resolveNode(ctx, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -53,7 +53,7 @@ func TestAnOverflowingConsumerNeverRefusesItsProvidersMachine(t *testing.T) {
if len(declared.withheld) != 1 || declared.withheld[0].Identity != "mesh_laptop_photoalbum" { if len(declared.withheld) != 1 || declared.withheld[0].Identity != "mesh_laptop_photoalbum" {
t.Fatalf("the overflowing consumer is not the one withheld: %+v", declared.withheld) t.Fatalf("the overflowing consumer is not the one withheld: %+v", declared.withheld)
} }
grants, _, _, err := grantsFor(ctx, open, "anchor") grants, _, _, err := grantsOf(ctx, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -79,7 +79,7 @@ func TestReplay201(t *testing.T) {
t.Fatalf("the send keeps the newer controller there: %v %v", keeps, err) t.Fatalf("the send keeps the newer controller there: %v %v", keeps, err)
} }
shelf := map[string]catalogue.Manifest{"mesh-controller": {Module: "mesh-controller", Version: "old"}} shelf := map[string]catalogue.Manifest{"mesh-controller": {Module: "mesh-controller", Version: "old"}}
if _, err := keepRecorded(keepingRecorded(judged), open, "anchor", shelf); err != nil || shelf["mesh-controller"].Version != "1" { if err := applyKept(keepingRecorded(judged), open, "anchor", keeps, shelf); err != nil || shelf["mesh-controller"].Version != "1" {
t.Fatalf("the send composes the kept build's manifest: %+v %v", shelf, err) t.Fatalf("the send composes the kept build's manifest: %+v %v", shelf, err)
} }
key := coreDowngradeObservation(lease.ComponentController, "anchor", "").Key() key := coreDowngradeObservation(lease.ComponentController, "anchor", "").Key()