diff --git a/cmd/mesh-builder/holder.go b/cmd/mesh-builder/holder.go index 777ca6f5..ff962c44 100644 --- a/cmd/mesh-builder/holder.go +++ b/cmd/mesh-builder/holder.go @@ -14,8 +14,8 @@ import ( "github.com/nats-io/nats.go/micro" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/builder" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/accept_outside.go b/cmd/mesh-controller/accept_outside.go index a4b8ae96..2489f542 100644 --- a/cmd/mesh-controller/accept_outside.go +++ b/cmd/mesh-controller/accept_outside.go @@ -6,7 +6,7 @@ import ( "sort" "strings" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) @@ -55,6 +55,8 @@ func offeredOutsideBy(ctx context.Context, provision string) (string, error) { // its next send would, and the machines that could not be resolved, with why. func consumersFrom(ctx context.Context, open *stores, provider, providerModule, provision string) ( []inventory.PairEnd, []string, error) { + // One reading of the mesh for every machine this looks at. + ctx = withInputCache(ctx) nodes, err := open.inventory.Nodes(ctx) if err != nil { return nil, nil, err @@ -62,7 +64,7 @@ func consumersFrom(ctx context.Context, open *stores, provider, providerModule, var out []inventory.PairEnd var unresolved []string for _, n := range nodes { - plan, _, err := planFor(ctx, open, n.Name) + plan, _, err := resolveNode(ctx, open, n.Name) if err != nil { unresolved = append(unresolved, fmt.Sprintf("%s: %s", n.Name, oneLine(err.Error()))) continue diff --git a/cmd/mesh-controller/acts.go b/cmd/mesh-controller/acts.go index 4f02544c..3989f0f8 100644 --- a/cmd/mesh-controller/acts.go +++ b/cmd/mesh-controller/acts.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) @@ -314,7 +314,7 @@ type afterAct struct { // after the write, which can only find more unmet than the closure would: a seat said held there is // held in the closure too. func resolvedAfter(ctx context.Context, open *stores, shelf map[string]catalogue.Manifest, node string) afterAct { - plan, _, err := planFor(ctx, open, node) + plan, _, err := resolveNode(ctx, open, node) if err == nil { out := afterAct{plan: plan, unheld: plan.Unheld} for _, m := range plan.Modules { @@ -355,6 +355,8 @@ func splitModules(field string) []string { // Nothing here can fail the act it reports on. A mesh that cannot be read is worth saying and is // not a reason to claim the assignment did not happen — it did. func blockedElsewhere(ctx context.Context, open *stores, except string) string { + // One reading of the mesh for every machine this looks at. + ctx = withInputCache(ctx) nodes, err := open.inventory.Nodes(ctx) if err != nil { return "\n\nThe rest of the mesh could not be checked: " + err.Error() @@ -364,7 +366,7 @@ func blockedElsewhere(ctx context.Context, open *stores, except string) string { if n.Name == except { continue } - if _, _, err := planFor(ctx, open, n.Name); err != nil { + if _, _, err := resolveNode(ctx, open, n.Name); err != nil { blocked[n.Name] = err.Error() } } diff --git a/cmd/mesh-controller/acts_test.go b/cmd/mesh-controller/acts_test.go index 2e8e6261..a7c6f819 100644 --- a/cmd/mesh-controller/acts_test.go +++ b/cmd/mesh-controller/acts_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" ) // What an assignment says about the machines it was not about. diff --git a/cmd/mesh-controller/addresses_integration_test.go b/cmd/mesh-controller/addresses_integration_test.go index 3d85873d..d8c507b2 100644 --- a/cmd/mesh-controller/addresses_integration_test.go +++ b/cmd/mesh-controller/addresses_integration_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/licences" ) diff --git a/cmd/mesh-controller/addresses_test.go b/cmd/mesh-controller/addresses_test.go index 5eb8d617..ec44be96 100644 --- a/cmd/mesh-controller/addresses_test.go +++ b/cmd/mesh-controller/addresses_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/adopting_test.go b/cmd/mesh-controller/adopting_test.go index 5561bb05..5a5191c1 100644 --- a/cmd/mesh-controller/adopting_test.go +++ b/cmd/mesh-controller/adopting_test.go @@ -12,10 +12,10 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" - "github.com/novox/mesh-controller/internal/overlay" ) // novox/hq ADR 0100: taking a module is its cutover; converging a node is one act, previewed, and diff --git a/cmd/mesh-controller/adoption.go b/cmd/mesh-controller/adoption.go index afa90a07..0ff12403 100644 --- a/cmd/mesh-controller/adoption.go +++ b/cmd/mesh-controller/adoption.go @@ -12,9 +12,9 @@ import ( "strings" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/overlay" ) // A node is adopted or converged (novox/hq ADR 0100), and it is said to be adopted wherever the @@ -243,7 +243,7 @@ func take(ctx context.Context, open *stores, node, module string, opts takeOptio return "", err } if !slices.Contains(assigned, module) { - if plan, _, err := planFor(ctx, open, node); err == nil { + if plan, _, err := resolveNode(ctx, open, node); err == nil { if why, runs := plan.Because[module]; runs { return "", fmt.Errorf("%w: %s runs on %s because %s — assign it to %s to take it", inventory.ErrNotAssigned, module, node, why, node) @@ -649,7 +649,7 @@ func converge(ctx context.Context, open *stores, node string, yes bool, digest s if err != nil { return "", err } - plan, settings, err := planFor(ctx, open, node) + plan, _, err := resolveNode(ctx, open, node) if err != nil { return "", err } @@ -706,11 +706,8 @@ func converge(ctx context.Context, open *stores, node string, yes bool, digest s filter) } - gens, err := generators(ctx, open) - if err != nil { - return "", err - } - with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Reading) + // What the filter is derived from: the rendering of this machine as a question reads it, with the private network. + with, err := renderingOf(ctx, open, node) if err != nil { return "", err } @@ -776,7 +773,7 @@ func converge(ctx context.Context, open *stores, node string, yes bool, digest s if _, err := inv.Assign(ctx, node, filter); err != nil { return "", err } - if _, _, err := planFor(ctx, open, node); err != nil { + if _, _, err := resolveNode(ctx, open, node); err != nil { _ = inv.Unassign(ctx, node, filter) return "", fmt.Errorf("%s cannot run %s, so it was not converged: %w", node, filter, err) } diff --git a/cmd/mesh-controller/agent_account_integration_test.go b/cmd/mesh-controller/agent_account_integration_test.go index 1d615a1c..ece0c1f5 100644 --- a/cmd/mesh-controller/agent_account_integration_test.go +++ b/cmd/mesh-controller/agent_account_integration_test.go @@ -7,8 +7,8 @@ import ( "testing" "time" + snapshot "git.novox.be/novox/mesh-sdk/go/facts" "github.com/novox/mesh-controller/internal/conditions" - snapshot "github.com/novox/mesh-controller/internal/facts" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-host/rootsearch" diff --git a/cmd/mesh-controller/agent_account_test.go b/cmd/mesh-controller/agent_account_test.go index 63d5dff9..c8db9085 100644 --- a/cmd/mesh-controller/agent_account_test.go +++ b/cmd/mesh-controller/agent_account_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" diff --git a/cmd/mesh-controller/asker_wire.go b/cmd/mesh-controller/asker_wire.go index 66651dc5..59c75efa 100644 --- a/cmd/mesh-controller/asker_wire.go +++ b/cmd/mesh-controller/asker_wire.go @@ -19,8 +19,8 @@ import ( "git.novox.be/novox/mesh-sdk/go/asks" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" diff --git a/cmd/mesh-controller/awaiting_push_integration_test.go b/cmd/mesh-controller/awaiting_push_integration_test.go index d3714080..b18d766f 100644 --- a/cmd/mesh-controller/awaiting_push_integration_test.go +++ b/cmd/mesh-controller/awaiting_push_integration_test.go @@ -8,10 +8,11 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/overlay" ) func TestAModuleAssignedAndNotPushedIsAwaitingAPushNotUncomposable(t *testing.T) { @@ -25,21 +26,13 @@ func TestAModuleAssignedAndNotPushedIsAwaitingAPushNotUncomposable(t *testing.T) // The read the rest of the mesh asks still refuses it, with the composer's typed error: nothing // can be compared about a secret that does not exist (status), and nothing here string-matches. - plan, settings, err := planFor(ctx, open, "laptop") - if err != nil { - t.Fatal(err) - } - gens, err := generators(ctx, open) - if err != nil { - t.Fatal(err) - } - _, err = declarationWith(ctx, open, "laptop", plan, settings, gens, Reading) + _, _, err := composeQuestion(ctx, open, "laptop", compose.Reading, false) var notMade *catalogue.NotMadeError if !errors.As(err, ¬Made) || notMade.Module != "keeper" || notMade.Name != "repository" { t.Fatalf("a read composition did not say which secret is not made, typed: %v", err) } // Foreseen, it composes, names what the push makes, and made nothing. - foreseen, err := declarationWith(ctx, open, "laptop", plan, settings, gens, Foreseeing) + foreseen, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false) if err != nil || len(foreseen.foreseen) != 1 || foreseen.foreseen[0] != "keeper/repository" { t.Fatalf("foreseen: %v %v", foreseen.foreseen, err) } diff --git a/cmd/mesh-controller/awaiting_push_test.go b/cmd/mesh-controller/awaiting_push_test.go index 58efd557..74b7097b 100644 --- a/cmd/mesh-controller/awaiting_push_test.go +++ b/cmd/mesh-controller/awaiting_push_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" ) @@ -29,7 +29,7 @@ func aKeeper() catalogue.Manifest { func pushedBy(t *testing.T, open *stores, node string) string { t.Helper() ctx := t.Context() - plan, settings, err := planFor(ctx, open, node) + plan, settings, err := resolveNode(ctx, open, node) if err != nil { t.Fatal(err) } @@ -54,11 +54,10 @@ func pushedBy(t *testing.T, open *stores, node string) string { if err != nil { t.Fatal(err) } - names := make([]string, 0, len(plan.Modules)) + builds = map[string]string{} for _, m := range plan.Modules { - names = append(names, m.Module) + builds[m.Module] = current[m.Module].Commit } - builds = carriedBuilds(names, nil, current, nil) } if err := open.inventory.RecordSent(ctx, record.ID, digest, builds); err != nil { t.Fatal(err) diff --git a/cmd/mesh-controller/away_integration_test.go b/cmd/mesh-controller/away_integration_test.go index 759b7ed9..72f18610 100644 --- a/cmd/mesh-controller/away_integration_test.go +++ b/cmd/mesh-controller/away_integration_test.go @@ -13,8 +13,8 @@ import ( "github.com/nats-io/nats.go" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/testbus" diff --git a/cmd/mesh-controller/backups_before_send_integration_test.go b/cmd/mesh-controller/backups_before_send_integration_test.go index 10da5daa..2baca74b 100644 --- a/cmd/mesh-controller/backups_before_send_integration_test.go +++ b/cmd/mesh-controller/backups_before_send_integration_test.go @@ -10,9 +10,9 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/nats-io/nats.go" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/testbus" diff --git a/cmd/mesh-controller/batches_integration_test.go b/cmd/mesh-controller/batches_integration_test.go index 880e9940..d7afe0af 100644 --- a/cmd/mesh-controller/batches_integration_test.go +++ b/cmd/mesh-controller/batches_integration_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/batches_test.go b/cmd/mesh-controller/batches_test.go index da7ad0e9..af52b8a9 100644 --- a/cmd/mesh-controller/batches_test.go +++ b/cmd/mesh-controller/batches_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/bindings.go b/cmd/mesh-controller/bindings.go index ea3309b8..aab111f1 100644 --- a/cmd/mesh-controller/bindings.go +++ b/cmd/mesh-controller/bindings.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" ) @@ -73,6 +73,8 @@ func reportKept(ctx context.Context, plan catalogue.Resolution) { // sent, on every machine — the resolver kept it there (said, urgent, until a person pins), or a pin // moves it (said, so the data goes first), and never anything else. func probeBindings(ctx context.Context, d *doctor) ([]conditions.Observation, error) { + // One reading of the mesh for every machine this looks at. + ctx = withInputCache(ctx) inv := d.open.inventory nodes, err := inv.Nodes(ctx) if err != nil { @@ -80,7 +82,7 @@ func probeBindings(ctx context.Context, d *doctor) ([]conditions.Observation, er } var out []conditions.Observation for _, n := range nodes { - plan, _, err := planFor(ctx, d.open, n.Name) + plan, _, err := resolveNode(ctx, d.open, n.Name) if err != nil { if unresolvable(err) { // D1 says it, with the binding that refused it when that is why. diff --git a/cmd/mesh-controller/bindings_integration_test.go b/cmd/mesh-controller/bindings_integration_test.go index 580c1fde..da08a10a 100644 --- a/cmd/mesh-controller/bindings_integration_test.go +++ b/cmd/mesh-controller/bindings_integration_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" ) @@ -37,7 +37,7 @@ func TestTheIncidentAConsumerStaysBesideItsDataUntilAPersonPinsIt(t *testing.T) assignAll([2]string{"laptop", "store"}, [2]string{"laptop", "resolver"}, [2]string{"laptop", "network"}, [2]string{"laptop", "board"}) - plan, _, err := planFor(ctx, open, "laptop") + plan, _, err := resolveNode(ctx, open, "laptop") if err != nil { t.Fatal(err) } @@ -64,7 +64,7 @@ func TestTheIncidentAConsumerStaysBesideItsDataUntilAPersonPinsIt(t *testing.T) if err := inv.Unassign(ctx, "laptop", "store"); err != nil { t.Fatal(err) } - if _, _, err := planFor(ctx, open, "laptop"); err == nil || !unresolvable(err) || + if _, _, err := resolveNode(ctx, open, "laptop"); err == nil || !unresolvable(err) || !strings.Contains(err.Error(), "board on laptop is bound to laptop/store") || !strings.Contains(err.Error(), "pin laptop postgres-database anchor store") { t.Fatalf("the consumer's store went and it was answered elsewhere: %v", err) @@ -74,7 +74,7 @@ func TestTheIncidentAConsumerStaysBesideItsDataUntilAPersonPinsIt(t *testing.T) if err := inv.PinProvision(ctx, "laptop", "postgres-database", "anchor", "store"); err != nil { t.Fatal(err) } - plan, _, err = planFor(ctx, open, "laptop") + plan, _, err = resolveNode(ctx, open, "laptop") if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-controller/bindings_test.go b/cmd/mesh-controller/bindings_test.go index 98cb6b69..2101e614 100644 --- a/cmd/mesh-controller/bindings_test.go +++ b/cmd/mesh-controller/bindings_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" ) diff --git a/cmd/mesh-controller/build.go b/cmd/mesh-controller/build.go index 5028bdf4..accc7e29 100644 --- a/cmd/mesh-controller/build.go +++ b/cmd/mesh-controller/build.go @@ -15,9 +15,9 @@ import ( "github.com/nats-io/nats.go" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/builder" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/build_seat_test.go b/cmd/mesh-controller/build_seat_test.go index e40ca0a5..26e5942d 100644 --- a/cmd/mesh-controller/build_seat_test.go +++ b/cmd/mesh-controller/build_seat_test.go @@ -3,7 +3,7 @@ package main import ( "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/build_source.go b/cmd/mesh-controller/build_source.go index da098867..9dc1dc05 100644 --- a/cmd/mesh-controller/build_source.go +++ b/cmd/mesh-controller/build_source.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) @@ -88,7 +88,7 @@ func newSourceForms(ctx context.Context, inv *inventory.Inventory) *sourceForms if world == nil { w := catalogue.World{} if shelf, err := inv.Catalogue(ctx); err == nil { - if read, err := theRestOfTheMesh(ctx, inv, shelf, ""); err == nil { + if read, err := restOfTheMeshOf(ctx, inv, shelf, ""); err == nil { w = read } } diff --git a/cmd/mesh-controller/bus_step.go b/cmd/mesh-controller/bus_step.go index e7785536..e52f97ce 100644 --- a/cmd/mesh-controller/bus_step.go +++ b/cmd/mesh-controller/bus_step.go @@ -11,7 +11,7 @@ import ( "github.com/nats-io/nats.go" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" diff --git a/cmd/mesh-controller/busobjects_test.go b/cmd/mesh-controller/busobjects_test.go index 349ab225..c40758f6 100644 --- a/cmd/mesh-controller/busobjects_test.go +++ b/cmd/mesh-controller/busobjects_test.go @@ -9,8 +9,8 @@ import ( "github.com/nats-io/nats.go" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" diff --git a/cmd/mesh-controller/busy_test.go b/cmd/mesh-controller/busy_test.go index 8399dcdc..4b8433e5 100644 --- a/cmd/mesh-controller/busy_test.go +++ b/cmd/mesh-controller/busy_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/chain_integration_test.go b/cmd/mesh-controller/chain_integration_test.go index 81866f2b..ea086c9c 100644 --- a/cmd/mesh-controller/chain_integration_test.go +++ b/cmd/mesh-controller/chain_integration_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/chain" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/lease" diff --git a/cmd/mesh-controller/changeplan.go b/cmd/mesh-controller/changeplan.go index 2b3ae219..f6d3461e 100644 --- a/cmd/mesh-controller/changeplan.go +++ b/cmd/mesh-controller/changeplan.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/changeplan_test.go b/cmd/mesh-controller/changeplan_test.go index 599cb20e..8e537227 100644 --- a/cmd/mesh-controller/changeplan_test.go +++ b/cmd/mesh-controller/changeplan_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/check.go b/cmd/mesh-controller/check.go index ce08029d..b6f605f8 100644 --- a/cmd/mesh-controller/check.go +++ b/cmd/mesh-controller/check.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/check_builds_integration_test.go b/cmd/mesh-controller/check_builds_integration_test.go index 37f33a6a..36a78eb6 100644 --- a/cmd/mesh-controller/check_builds_integration_test.go +++ b/cmd/mesh-controller/check_builds_integration_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/check_builds_test.go b/cmd/mesh-controller/check_builds_test.go index 7a339b51..8c4d0ccf 100644 --- a/cmd/mesh-controller/check_builds_test.go +++ b/cmd/mesh-controller/check_builds_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/builder" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/check_here.go b/cmd/mesh-controller/check_here.go index 201e1dec..51e7c786 100644 --- a/cmd/mesh-controller/check_here.go +++ b/cmd/mesh-controller/check_here.go @@ -11,8 +11,8 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/builder" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/check_siblings_test.go b/cmd/mesh-controller/check_siblings_test.go index b35e8e60..379ecd98 100644 --- a/cmd/mesh-controller/check_siblings_test.go +++ b/cmd/mesh-controller/check_siblings_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/check_test.go b/cmd/mesh-controller/check_test.go index eecf6139..608b6b69 100644 --- a/cmd/mesh-controller/check_test.go +++ b/cmd/mesh-controller/check_test.go @@ -8,8 +8,8 @@ import ( "testing" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/beside" - "github.com/novox/mesh-controller/internal/catalogue" ) // The check anybody can run is the check registration runs (novox/hq issue 148, ADR 0037): a manifest diff --git a/cmd/mesh-controller/checks.go b/cmd/mesh-controller/checks.go index eda91b65..007b4adb 100644 --- a/cmd/mesh-controller/checks.go +++ b/cmd/mesh-controller/checks.go @@ -14,8 +14,8 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) @@ -75,7 +75,7 @@ type checkScope struct { // Mesh says the repository is the mesh's: modules are built from it on some branch, its owner is the // core's, or the change adds a module to it. Mesh bool - // Judge is who judges the gate (link.JudgeSelf, link.JudgeValidator, or the running controller). + // Judge is who judges the gate (link.JudgeSelf, or the running controller). Judge string // From is a module built from the repository, for how the mesh clones it; nil when none is. From *inventory.Entry @@ -101,10 +101,6 @@ func pullScope(p link.PullUpdated, entries []inventory.Entry, read map[string][] switch e.Manifest.Module { case "mesh-controller": s.Judge = link.JudgeSelf - case "mesh-host": - if s.Judge == "" { - s.Judge = link.JudgeValidator - } } } for _, d := range r.Added { @@ -281,7 +277,7 @@ func checkRequestFor(ctx context.Context, open *stores, p link.PullUpdated, scop for _, e := range entries { shelf[e.Manifest.Module] = e.Manifest } - world, err := theRestOfTheMesh(ctx, open.inventory, shelf, "") + world, err := restOfTheMesh(ctx, open, shelf, "") if err != nil { return link.BuildRequest{}, err } diff --git a/cmd/mesh-controller/checks_queue_test.go b/cmd/mesh-controller/checks_queue_test.go index d79c6207..b9fb48f2 100644 --- a/cmd/mesh-controller/checks_queue_test.go +++ b/cmd/mesh-controller/checks_queue_test.go @@ -10,8 +10,8 @@ import ( "github.com/nats-io/nats.go" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/checks_test.go b/cmd/mesh-controller/checks_test.go index 24e37f3d..27bc6f8f 100644 --- a/cmd/mesh-controller/checks_test.go +++ b/cmd/mesh-controller/checks_test.go @@ -53,8 +53,8 @@ func TestThePullRequestIsMappedOntoTheModuleGraph(t *testing.T) { "", "modules/newmod", "modules/newmod/module.json", true, ""}, {"the controller judges itself", pull("novox", "mesh-controller", "main", []string{"cmd/x.go"}), "mesh-controller", "", "module.json", true, "self"}, - {"the node-engine is judged with its validator", pull("novox", "mesh-host", "main", []string{"validate/v.go"}), - "mesh-host", "", "module.json", true, "validator"}, + {"the node-engine is judged by the controller the mesh runs, as any module is (issue 1391)", + pull("novox", "mesh-host", "main", []string{"validate/v.go"}), "mesh-host", "", "module.json", true, ""}, {"the core's owner, no module: the mesh's, its own check alone", pull("novox", "hq", "main", []string{"README.md"}), "", "", "", true, ""}, {"a branch nothing is built from: the mesh's repository, no module", pull("novox", "photos", "master", diff --git a/cmd/mesh-controller/compose_callers_test.go b/cmd/mesh-controller/compose_callers_test.go new file mode 100644 index 00000000..626860dd --- /dev/null +++ b/cmd/mesh-controller/compose_callers_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "go/ast" + "go/parser" + "go/token" + "path/filepath" + "sort" + "strings" + "testing" +) + +// 1389: the controller's own composition is gone. No function of the package is named for it any more, and no function +// calls one by that name: a name reappearing is a second copy of what mesh-sdk's go/compose holds (novox/hq ADR 0453). +func TestTheControllersOwnCompositionIsGone(t *testing.T) { + gone := map[string]bool{"planFor": true, "declarationWith": true, "composedAndValidated": true, "generators": true, + "renderingFor": true, "theRestOfTheMesh": true, "whoResolves": true, "grantsFor": true, "namesInTheMesh": true, + "onThePrivateNetwork": true, "passSeatsOn": true, "theMeshPassesSeatsOn": true, "assignmentsOf": true, + "licencesFor": true, "seatsOn": true, "composeBusUsers": true, "adoptionFor": true, "localNetworksOf": true} + files, err := filepath.Glob("*.go") + if err != nil { + t.Fatal(err) + } + fset := token.NewFileSet() + var found []string + for _, f := range files { + if strings.HasSuffix(f, "_test.go") { + continue + } + file, err := parser.ParseFile(fset, f, nil, 0) + if err != nil { + t.Fatal(err) + } + for _, d := range file.Decls { + fn, ok := d.(*ast.FuncDecl) + if !ok { + continue + } + if gone[fn.Name.Name] { + found = append(found, f+": "+fn.Name.Name+" is declared") + } + if fn.Body == nil { + continue + } + ast.Inspect(fn.Body, func(n ast.Node) bool { + if c, ok := n.(*ast.CallExpr); ok { + if id, ok := c.Fun.(*ast.Ident); ok && gone[id.Name] { + found = append(found, f+": "+fn.Name.Name+" calls "+id.Name) + } + } + return true + }) + } + } + sort.Strings(found) + for _, one := range found { + t.Errorf("%s: the controller's own composition is back; compose through mesh-sdk (compose_sdk.go)", one) + } +} diff --git a/cmd/mesh-controller/compose_fallbacks_test.go b/cmd/mesh-controller/compose_fallbacks_test.go new file mode 100644 index 00000000..04537cc4 --- /dev/null +++ b/cmd/mesh-controller/compose_fallbacks_test.go @@ -0,0 +1,114 @@ +package main + +import ( + "encoding/base64" + "errors" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "github.com/novox/mesh-controller/internal/broker" +) + +// aCoreInput is an Input of one machine that takes a core module whose credential the vault has not boxed: the composition +// stands in a vault box for it, and the machine holds the controller's own seal of it. +func aCoreInput(t *testing.T) (*meshInput, string) { + t.Helper() + raw := make([]byte, 32) + raw[0] = 9 + key := base64.StdEncoding.EncodeToString(raw) + client := catalogue.Manifest{Module: "mesh-provisioner-client", Version: "1", + Claims: []catalogue.Claim{{Name: broker.NodeProvisionerSeat, Scope: catalogue.ScopeNode}}, + OwnSecrets: catalogue.OwnSecrets{broker.ProvisionerSecret: {Path: "/var/lib/mesh-provisioner-client/broker"}}} + return &meshInput{Input: compose.Input{ + Catalogue: map[string]catalogue.Manifest{client.Module: client}, + Nodes: []compose.Node{{Name: "laptop", Assigned: []string{client.Module}, SealingKey: key, Stated: true, + Contract: compose.VaultBoxContract}}, + OwnSecrets: []compose.OwnSecretRow{{Node: "laptop", Module: client.Module, Name: broker.ProvisionerSecret, + Origin: "made", SealedTo: key, Sealed: "SEALED-BY-THE-CONTROLLER"}}, + }}, client.Module +} + +// 1385 (the lead's answer 1): where the vault is silent and no box is kept, the declaration carries the controller's own seal +// for the core credential, as ADR 0396 has it, and stands in for nothing. It failed first: the composition's declaration +// carried the stand-in box, and a send was refused. +func TestACoreCredentialTheVaultDidNotBoxIsSealedByTheController(t *testing.T) { + in, module := aCoreInput(t) + d, _, err := compose.ComposeIn(in.Input, "laptop", compose.Foreseeing) + if err != nil { + t.Fatal(err) + } + if len(d.CoreBoxesWanted) != 1 || len(stillForeseen(d)) == 0 { + t.Fatalf("the composition does not stand in for the vault's box: %v", d.CoreBoxesWanted) + } + sealed, _, err := sealedByTheController(in, "laptop", d, []string{module}) + if err != nil { + t.Fatal(err) + } + if left := stillForeseen(sealed); len(left) != 0 { + t.Fatalf("the declaration still stands in for %v", left) + } + var file map[string]any + for _, r := range sealed.Resources { + if r["id"] == module+"."+catalogue.NeedID(broker.ProvisionerSecret) { + file = r + } + } + if file == nil || file["sealed"] != "SEALED-BY-THE-CONTROLLER" || file[catalogue.VaultSealedField] != nil { + t.Fatalf("the core credential is carried as %v: the controller's own seal, not the vault's box", file) + } + // And a module the vault did answer for is left as it is. + other, _, err := sealedByTheController(in, "laptop", d, []string{"another-module"}) + if err != nil { + t.Fatal(err) + } + if len(stillForeseen(other)) == 0 { + t.Fatal("a module the vault answered for was sealed by the controller") + } +} + +// 1383 (the lead's answer 4): the machine that runs the bus is refused where no user has a credential, with busUserList's +// words, and a machine that does not run the bus is not. It failed first: the Input stood a list in and the bus node composed. +func TestTheMachineThatRunsTheBusIsRefusedWhereNoUserHasACredential(t *testing.T) { + bus := catalogue.Manifest{Module: "nats", Version: "1", BusUsers: "/etc/nats/users.conf", + Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}} + other := catalogue.Manifest{Module: "files", Version: "1"} + why := errors.New("this machine runs the bus and not one user has a credential, so the composed list would refuse every connection in the mesh") + in := &meshInput{busRefused: why, Input: compose.Input{ + Catalogue: map[string]catalogue.Manifest{"nats": bus, "files": other}, + Nodes: []compose.Node{{Name: "anchor", Assigned: []string{"nats"}}, {Name: "laptop", Assigned: []string{"files"}}}, + }} + if err := refusedBus(in, "anchor"); err == nil || !strings.Contains(err.Error(), "not one user has a credential") { + t.Fatalf("the machine that runs the bus is not refused: %v", err) + } + if err := refusedBus(in, "laptop"); err != nil { + t.Fatalf("a machine that does not run the bus is refused: %v", err) + } + if err := refusedBus(&meshInput{Input: in.Input}, "anchor"); err != nil { + t.Fatalf("a bus with users is refused: %v", err) + } +} + +// The controller's seal replaces the stand-in box whatever path the resource carries: the path is the manifest's with the +// machine's words filled in, not the manifest's own. It failed first, with the path compared (the live try: "composed twice and +// still names ... asked of the vault by the next send"). +func TestTheControllersSealReplacesTheBoxWhateverPathTheFileCarries(t *testing.T) { + in, module := aCoreInput(t) + d, _, err := compose.ComposeIn(in.Input, "laptop", compose.Foreseeing) + if err != nil { + t.Fatal(err) + } + for _, r := range d.Resources { + if _, box := r[catalogue.VaultSealedField]; box { + r["path"] = "/home/someone/.var/lib/mesh-provisioner-client/broker" + } + } + sealed, _, err := sealedByTheController(in, "laptop", d, []string{module}) + if err != nil { + t.Fatal(err) + } + if left := stillForeseen(sealed); len(left) != 0 { + t.Fatalf("the declaration still stands in for %v", left) + } +} diff --git a/cmd/mesh-controller/compose_helpers_test.go b/cmd/mesh-controller/compose_helpers_test.go new file mode 100644 index 00000000..918f2dfa --- /dev/null +++ b/cmd/mesh-controller/compose_helpers_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "context" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" +) + +// grantsOf is the grants a provider creates for its consumers, the consumers whose identity overflows the provision's bound +// and the credentials bound to another consumer, as mesh-sdk's rendering gives them for the machine (it replaced the +// controller's grantsFor, issue 1389). +func grantsOf(ctx context.Context, open *stores, node string) ([]catalogue.Grant, []catalogue.Overflow, []catalogue.Unbound, error) { + in, err := inputFor(ctx, open) + if err != nil { + return nil, nil, nil, err + } + rendered, err := compose.Render(in.Input, node, compose.Reading) + if err != nil { + return nil, nil, nil, err + } + return rendered.Rendering.Grants, rendered.Withheld, rendered.Unbound, nil +} + +// madeAndRendered sends to the machine (so everything a send makes is made) and answers its rendering as a question reads it +// afterwards: what the controller's Allocating rendering was. +func madeAndRendered(ctx context.Context, open *stores, node string) (catalogue.Rendering, error) { + if _, err := composeSend(ctx, open, node); err != nil { + return catalogue.Rendering{}, err + } + forgetInput(ctx) + return renderingOf(ctx, open, node) +} diff --git a/cmd/mesh-controller/compose_input.go b/cmd/mesh-controller/compose_input.go new file mode 100644 index 00000000..fc102b93 --- /dev/null +++ b/cmd/mesh-controller/compose_input.go @@ -0,0 +1,436 @@ +package main + +import ( + "context" + "os" + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "git.novox.be/novox/mesh-sdk/go/overlay" + "github.com/novox/mesh-controller/internal/inventory" +) + +// The controller's adapter from its stores to mesh-sdk's composition input (novox/hq ADR 0453 clause 3, design 47 R4b, +// issues 1382 and 1383). +// +// **The adapter reads, and perform writes.** readComposeInput asks the stores once for everything the composition reads +// and returns compose.Input, which the composition never changes; it makes, seals, allocates and records nothing. A +// pair credential, a port, an own secret or a core box that the next send would make is not made here: the composition +// names it foreseen, and compose_perform.go makes it for a send, then reads the stores again and composes again. A +// question (status, facts, probes, the merge gate's reading) never performs. +// +// **The Input is built once per operation.** A verb that composes every node reads the stores once and hands the Input to +// each node's composition of it. + +// composeInputBuilt is called once for each Input the adapter builds: the seam a test counts the reads of an operation by +// (issue 1382, criterion 5). +var composeInputBuilt = func() {} + +// readComposeInput reads the stores once and returns what the composition needs. ctx carries what a send keeps (the +// recorded modules kept at the build their machine runs, recorded_kept.go): the same contexts every send composes under. +func readComposeInput(ctx context.Context, open *stores) (*meshInput, error) { + in, shape, err := readMeshAndNodes(ctx, open) + if err != nil { + return nil, err + } + // What each node resolves to, with no credential given (the settings are read before it): the modules whose settings are read, and the + // needs whose pair credentials are asked of the stores. Nothing is written by it. + unread, err := readSettings(ctx, open, &in) + if err != nil { + return nil, err + } + plans := map[string]compose.Resolved{} + for _, n := range in.Nodes { + if shape.refused[n.Name] != nil { + continue // refused for what it keeps: nothing of it is read further + } + resolved, err := compose.Resolve(in, n.Name) + switch { + case compose.IsUnresolvable(err): + // Their set does not compose: nothing of theirs is read further, as the controller never read a thing for a + // node that did not resolve. + continue + case err != nil: + return nil, err + } + plans[n.Name] = resolved + } + if err := unreadSettings(unread, plans); err != nil { + return nil, err + } + var busRefused error + wouldMake, err := readCredentialsAndBuilds(ctx, open, &in, plans, shape, &busRefused) + if err != nil { + return nil, err + } + composeInputBuilt() + return &meshInput{Input: in, wouldMake: wouldMake, busRefused: busRefused, refused: shape.refused}, nil +} + +// meshInput is the Input an operation composes over, and the pair credentials its reading found none held for: the ones the +// next send makes, which perform makes. +type meshInput struct { + compose.Input + wouldMake []inventory.PairCredential + // busRefused is why the bus's user list cannot be composed (no user has a credential): the machine that runs the bus is + // refused with it, as the controller's own composition refused it, and no other machine is. + busRefused error + // refused is, by node, why that machine alone cannot be composed (nodeRefusal). + refused map[string]error +} + +// nodeOf is the node of that name in an Input. +func nodeOf(in compose.Input, name string) (compose.Node, bool) { + for _, n := range in.Nodes { + if n.Name == name { + return n, true + } + } + return compose.Node{}, false +} + +// inputShape is what the first half of the reading found that the second half needs. +type inputShape struct { + sent map[string]map[string]string // by node: module to the commit last sent + kept map[string]map[string]string // by node: module to the commit kept + current map[string]inventory.CurrentBuild + // refused is, by node, why a recorded module cannot be kept at the build the machine runs (keepRecorded's refusal, in + // its words): that machine alone is refused with it, as planFor refused it, and nothing else of the mesh is. + refused map[string]error +} + +// readMeshAndNodes is the mesh-wide and per-node half of the Input (issue 1382). +func readMeshAndNodes(ctx context.Context, open *stores) (compose.Input, inputShape, error) { + inv := open.inventory + var in compose.Input + shape := inputShape{sent: map[string]map[string]string{}, kept: map[string]map[string]string{}, refused: map[string]error{}} + + shelf, err := inv.Catalogue(ctx) + if err != nil { + return in, shape, err + } + in.Catalogue = shelf + if in.Holdings, err = inv.Holdings(ctx); err != nil { + return in, shape, err + } + if in.Licences, err = readLicences(ctx, open); err != nil { + return in, shape, err + } + + nodes, err := inv.Nodes(ctx) + if err != nil { + return in, shape, err + } + places, err := inv.Overlays(ctx) + if err != nil { + return in, shape, err + } + placed := map[string]inventory.Overlay{} + for _, p := range places { + placed[p.Name] = p + } + if shape.current, err = inv.CurrentBuilds(ctx); err != nil { + return in, shape, err + } + + // The range the mesh allocates from and what its internal names end in, read from the environment here and nowhere else. + in.OverlayCIDR = strings.TrimSpace(os.Getenv(OverlayCIDRVar)) + in.InternalSuffix = overlay.Suffix() + if in.Tunnels, err = readTunnels(ctx, inv); err != nil { + return in, shape, err + } + if in.Adopted, err = readAdopted(ctx, inv); err != nil { + return in, shape, err + } + carried, err := inv.CarriedPeers(ctx) + if err != nil { + return in, shape, err + } + for _, c := range carried { + in.CarriedPeers = append(in.CarriedPeers, compose.CarriedPeer{ + PublicKey: c.PublicKey, Address: c.Address, EnrolledAs: c.EnrolledAs, Named: c.Named}) + } + if in.Joining, err = inv.NodesWithALiveToken(ctx); err != nil { + return in, shape, err + } + + keepsSecrets := false + for _, m := range shelf { + keepsSecrets = keepsSecrets || m.Keeps != "" + } + + for _, n := range nodes { + var refusal error + node, err := readNode(ctx, open, n, placed[n.Name], &refusal) + if err != nil { + return in, shape, err + } + if refusal != nil { + shape.refused[n.Name] = refusal + } + shape.sent[n.Name], shape.kept[n.Name] = node.SentBuilds, node.KeptBuilds + in.Nodes = append(in.Nodes, node) + } + if keepsSecrets { + if in.OperatorExport, err = inv.OperatorExport(ctx); err != nil { + return in, shape, err + } + } + return in, shape, nil +} + +// readNode is one node of the Input: everything the composition reads about it, in the order the controller read it. +func readNode(ctx context.Context, open *stores, n inventory.Node, place inventory.Overlay, + refused *error) (compose.Node, error) { + inv := open.inventory + out := compose.Node{ + Name: n.Name, + Place: compose.Place{Key: place.Key, Endpoint: place.Endpoint, Site: place.Site, Hub: place.Hub, + Address: place.Address}, + Account: n.Account, AccountHome: n.AccountHome, AgentAccount: n.AgentAccount, AgentAccountHome: n.AgentAccountHome, + Adopted: n.Adopted, + } + var err error + if out.Assigned, err = inv.Assigned(ctx, n.Name); err != nil { + return out, err + } + if out.Capabilities, err = inv.ProfileOf(ctx, n.Name); err != nil { + return out, err + } + if out.PublicDomain, err = inv.PublicDomainOf(ctx, n.Name); err != nil { + return out, err + } + if out.Pins, err = inv.PinsFor(ctx, n.Name); err != nil { + return out, err + } + if out.Bound, err = inv.BindingsFor(ctx, n.Name); err != nil { + return out, err + } + given, err := inv.PortsFor(ctx, n.Name) + if err != nil { + return out, err + } + for _, a := range given { + if out.Ports == nil { + out.Ports = map[string]map[int]int{} + } + if out.Ports[a.Module] == nil { + out.Ports[a.Module] = map[int]int{} + } + out.Ports[a.Module][a.Wanted] = a.Machine + } + if out.SealingKey, err = inv.SealingKeyOf(ctx, n.Name); err != nil { + return out, err + } + if n.Adopted { + if out.Taken, err = inv.Taken(ctx, n.Name); err != nil { + return out, err + } + } + if out.OutwardLinks, err = inv.OutwardLinksOf(ctx, n.Name); err != nil { + return out, err + } + account, had, err := inv.MachineOf(ctx, n.Name) + if err != nil { + return out, err + } + if had && account.Machine != nil { + out.NetworksSaid = true + for _, a := range account.Machine.Networks { + out.Networks = append(out.Networks, catalogue.AttachedNetwork{Link: a.Link, Network: a.Network}) + } + } + if out.Sequence, err = inv.Sequence(ctx, n.ID); err != nil { + return out, err + } + if out.Epoch, err = inv.SentEpoch(ctx, n.ID); err != nil { + return out, err + } + if out.Generation, err = inv.SentGeneration(ctx, n.ID); err != nil { + return out, err + } + if out.ReadsModules, err = inv.ReadsModules(ctx, n.ID); err != nil { + return out, err + } + if out.SentBuilds, out.SentKnown, err = inv.SentBuilds(ctx, n.Name); err != nil { + return out, err + } + runs, err := inv.AssignmentRuns(ctx, n.Name) + if err != nil { + return out, err + } + if len(runs) > 0 { + out.Runs = make(map[string]compose.Run, len(runs)) + for module, ref := range runs { + out.Runs[module] = compose.Run{Commit: ref.Commit, Build: ref.Build} + } + } + // A recorded module kept at the build this machine runs, in the context a send composes under (recorded_kept.go): its + // manifest and the commit, applied to this node's own resolution only. + // What cannot be kept refuses this machine alone (keepRecorded's refusal, as planFor returned it). + kept, err := recordedKept(ctx, open, n.Name) + if err != nil { + *refused = err + } else if len(kept) > 0 { + out.KeptBuilds = kept + if out.Kept, err = keptManifests(ctx, open, n.Name, kept); err != nil { + *refused = err + out.KeptBuilds, out.Kept = nil, nil + } + } + stated, has, err := inv.HealthOf(ctx, n.Name) + if err != nil { + return out, err + } + out.Stated = has + if has { + out.Contract = stated.Contract + } + return out, nil +} + +// keptManifests is the manifest of each module kept at the build a machine runs: keepRecorded's reads, and its refusals in its +// words, without putting any of it into a shelf (that is the node's own Kept, applied by the composition). +func keptManifests(ctx context.Context, open *stores, node string, kept map[string]string) (map[string]catalogue.Manifest, error) { + // applyKept puts each manifest into the shelf it is given and refuses a build that cannot be read; the shelf here is a + // scratch one, whose entries are the kept manifests. + scratch := map[string]catalogue.Manifest{} + if err := applyKept(ctx, open, node, kept, scratch); err != nil { + return nil, err + } + return scratch, nil +} + +func readTunnels(ctx context.Context, inv *inventory.Inventory) (map[string]compose.FoundTunnel, error) { + found, err := inv.Tunnels(ctx) + if err != nil { + return nil, err + } + if len(found) == 0 { + return nil, nil + } + out := make(map[string]compose.FoundTunnel, len(found)) + for name, t := range found { + out[name] = compose.FoundTunnel{Tunnel: tunnelOf(t.Tunnel), NodeAdopted: t.NodeAdopted} + } + return out, nil +} + +func readAdopted(ctx context.Context, inv *inventory.Inventory) (*compose.AdoptedTunnel, error) { + tunnel, hub, adopted, err := inv.AdoptedTunnel(ctx) + if err != nil { + return nil, err + } + if hub == "" { + return nil, nil + } + return &compose.AdoptedTunnel{Tunnel: tunnelOf(tunnel), Hub: hub, Adopted: adopted}, nil +} + +func tunnelOf(t inventory.Tunnel) compose.Tunnel { + out := compose.Tunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU, + Address: t.Address, Range: t.Range, PublicKey: t.PublicKey} + for _, p := range t.Peers { + out.Peers = append(out.Peers, compose.TunnelPeer{PublicKey: p.PublicKey, Address: p.Address}) + } + return out +} + +// readLicences is every licence the mesh holds, with who uses it and the key sealed to each holder: given even where empty, +// so a holder the mesh holds no key for composes as that, and not as a key that was not asked for. +func readLicences(ctx context.Context, open *stores) ([]compose.Licence, error) { + held, err := open.Licences(ctx) + if err != nil { + return nil, err + } + all, err := held.All(ctx) + if err != nil { + return nil, err + } + var out []compose.Licence + for _, one := range all { + l := compose.Licence{Name: one.Name, Serves: one.Serves, Keys: map[compose.Holder]string{}} + holders, err := held.HoldersOf(ctx, one.Name) + if err != nil { + return nil, err + } + for _, h := range holders { + holder := compose.Holder{Node: h.Node, Module: h.Module} + l.Holders = append(l.Holders, holder) + key, err := held.KeyFor(ctx, one.Name, h.Node, h.Module) + if err != nil { + return nil, err + } + l.Keys[holder] = key + } + node, module, err := held.ManagerOf(ctx, one.Name) + if err != nil { + return nil, err + } + l.Manager = compose.Holder{Node: node, Module: module} + out = append(out, l) + } + return out, nil +} + +// readSettings is each node's settings layers for every module that has any, read BEFORE any node is resolved: the first pass +// resolves a node whose set takes a provider's served facts from the node that runs it, with that node's settings on them +// (servedOnNode), so a mesh whose provider says ${setting:domain} resolves only with the settings in the Input. A module's +// settings that fail to read are returned in unread, by node and module, and are an error only for a module a node resolves to +// (unreadSettings): the controller read none for a module nothing runs. +func readSettings(ctx context.Context, open *stores, in *compose.Input) (unread map[string]map[string]error, err error) { + have, err := open.inventory.ModulesWithSettings(ctx) + if err != nil { + return nil, err + } + names := make([]string, 0, len(have)) + for module := range have { + if _, onShelf := in.Catalogue[module]; onShelf { + names = append(names, module) + } + } + sort.Strings(names) + for i := range in.Nodes { + n := &in.Nodes[i] + for _, module := range names { + layers, err := open.inventory.SettingsFor(ctx, n.Name, module) + if err != nil { + if unread == nil { + unread = map[string]map[string]error{} + } + if unread[n.Name] == nil { + unread[n.Name] = map[string]error{} + } + unread[n.Name][module] = err + continue + } + if len(layers) == 0 { + continue + } + if n.Settings == nil { + n.Settings = map[string][]catalogue.Layer{} + } + n.Settings[module] = layers + } + } + return unread, nil +} + +// unreadSettings is the error of a module's settings that did not read, for a module the node resolves to. +func unreadSettings(unread map[string]map[string]error, plans map[string]compose.Resolved) error { + names := make([]string, 0, len(plans)) + for node := range plans { + names = append(names, node) + } + sort.Strings(names) + for _, node := range names { + for _, m := range plans[node].Plan.Modules { + if err := unread[node][m.Module]; err != nil { + return err + } + } + } + return nil +} diff --git a/cmd/mesh-controller/compose_input_credentials.go b/cmd/mesh-controller/compose_input_credentials.go new file mode 100644 index 00000000..a15b28d4 --- /dev/null +++ b/cmd/mesh-controller/compose_input_credentials.go @@ -0,0 +1,285 @@ +package main + +import ( + "context" + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "git.novox.be/novox/mesh-sdk/go/overlay" + "github.com/novox/mesh-controller/internal/inventory" +) + +// The second half of the adapter (issue 1383): the credential and build fields of the Input, read and never made. + +// readCredentialsAndBuilds fills what the first half left: the credentials, the own secrets, the vault's boxes, the bus, +// each node's serving key and certificate, and the builds. +func readCredentialsAndBuilds(ctx context.Context, open *stores, in *compose.Input, plans map[string]compose.Resolved, + shape inputShape, busRefused *error) ([]inventory.PairCredential, error) { + if err := readIdentity(ctx, open, in); err != nil { + return nil, err + } + wouldMake, err := readCredentials(ctx, open, in, plans, busRefused) + if err != nil { + return nil, err + } + return wouldMake, readBuilds(ctx, open, in, plans, shape) +} + +// readIdentity is the mesh's authority and each node's serving key and the certificate kept for it, for a mesh in which a +// module asks for a certificate. It establishes and issues nothing: a certificate not kept yet is composed with a stand-in, and +// issued by the send (certificateFor). +func readIdentity(ctx context.Context, open *stores, in *compose.Input) error { + needs := false + for _, m := range in.Catalogue { + needs = needs || m.Certificate != nil + } + if !needs { + return nil + } + ident, err := open.Identity(ctx) + if err != nil { + return err + } + if certificate, known, err := ident.KnownAuthority(ctx); err != nil { + return err + } else if known { + in.Authority = certificate + } + nodes, err := open.inventory.Nodes(ctx) + if err != nil { + return err + } + ids := map[string]string{} + for _, n := range nodes { + ids[n.Name] = n.ID + } + for i := range in.Nodes { + n := &in.Nodes[i] + if n.ServingKey, err = ident.ServingKeyOf(ctx, ids[n.Name]); err != nil { + return err + } + if n.ServingKey == "" { + continue + } + if certificate, known, err := ident.KeptCertificate(ctx, overlay.InternalName(n.Name), n.ServingKey); err != nil { + return err + } else if known { + n.Certificate = certificate + } + } + return nil +} + +// readCredentials is how each pair credential a node takes from elsewhere stands, the credentials made for each provider's +// consumers, the own secrets, the vault's boxes and the bus. It reads and never makes: a credential a send would make is +// left out (the composition's would-make), a credential only a person gives and nobody gave is not-given, and one the send +// would be refused is refused with the controller's words. +func readCredentials(ctx context.Context, open *stores, in *compose.Input, plans map[string]compose.Resolved, + busRefused *error) ([]inventory.PairCredential, error) { + var wouldMake []inventory.PairCredential + inv := open.inventory + shelf := in.Catalogue + nodeOf := map[string]compose.Node{} + for _, n := range in.Nodes { + nodeOf[n.Name] = n + } + names := make([]string, 0, len(plans)) + for name := range plans { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + resolved := plans[name] + nodeShelf := shelfWith(shelf, nodeOf[name].Kept) + for _, need := range resolved.Plan.Needs { + if need.ByRecord { + continue + } + p := inventory.PairCredential{Name: need.Name, Consumer: name, ConsumerModule: need.For, + Provider: need.From, Local: need.Local} + if module, outside := issuedOutsideBy(nodeShelf, need); outside { + p.Kind, p.ProviderModule = inventory.PairOutside, module + } else if need.SharedOwn != "" { + p.Kind, p.Own = inventory.PairShared, need.SharedOwn + p.ProviderModule = sharedProviderModule(resolved.Plan, shelf, need) + } + state, err := inv.PairCredentialState(ctx, p) + if err != nil { + return nil, err + } + c := compose.Credential{Provision: need.Name, Consumer: name, ConsumerModule: need.For, + Provider: need.From, Local: need.Local} + switch state.State { + case inventory.PairHeld: + c.State, c.Sealed = compose.CredentialHeld, state.Sealed + case inventory.PairNotGiven: + c.State, c.Why = compose.CredentialNotGiven, state.Why + case inventory.PairRefused: + c.State, c.Why = compose.CredentialRefused, state.Why + default: + // Left out: a need with no entry is the credential the next send makes, and perform makes it. + wouldMake = append(wouldMake, p) + continue + } + in.Credentials = append(in.Credentials, c) + } + } + + // The credentials made for each provider's consumers, so the grants it must create are read from them. + for _, n := range in.Nodes { + rows, err := inv.SecretsFrom(ctx, n.Name) + if err != nil { + return nil, err + } + for _, s := range rows { + in.Issued = append(in.Issued, compose.Issued{Provision: s.Name, Consumer: s.Consumer, + ConsumerModule: s.ConsumerModule, Local: s.Local, Provider: n.Name, ForProvider: s.ForProvider}) + } + } + own, err := inv.OwnSecretRows(ctx) + if err != nil { + return nil, err + } + for _, r := range own { + in.OwnSecrets = append(in.OwnSecrets, compose.OwnSecretRow{Node: r.Node, Module: r.Module, Name: r.Name, + Origin: r.Origin, SealedTo: r.SealedTo, Sealed: r.Sealed}) + } + for _, n := range in.Nodes { + boxes, err := inv.KeptCoreBoxes(ctx, n.Name) + if err != nil { + return nil, err + } + for module, byName := range boxes { + for name, b := range byName { + in.KeptCoreBoxes = append(in.KeptCoreBoxes, compose.CoreBox{Node: n.Name, Module: module, Name: name, + Generation: b.Generation, SealedTo: b.SealedTo, Box: b.Box}) + } + } + } + sort.Slice(in.KeptCoreBoxes, func(i, j int) bool { + a, b := in.KeptCoreBoxes[i], in.KeptCoreBoxes[j] + if a.Node != b.Node { + return a.Node < b.Node + } + if a.Module != b.Module { + return a.Module < b.Module + } + return a.Name < b.Name + }) + + // The bus: its user list as the controller composes it, for the machine that runs it, and who the mesh has minted a + // credential for. + var bus []catalogue.Manifest + for _, m := range shelf { + if m.BusUsers != "" && m.ClaimsSeat("mesh-broker") { + bus = append(bus, m) + } + } + if len(bus) > 0 { + list, missing, err := busUserList(ctx, inv, bus) + if err != nil && !strings.Contains(err.Error(), "not one user has a credential") { + return nil, err + } + if err != nil { + // The machine that runs the bus is refused, as the controller's own composition refused it: a list that + // refuses every connection in the mesh is never composed (refusedBus); the other machines compose. + *busRefused = err + } + in.BusUsers, in.BusMissing = list, missing + } + if in.BusMinted, err = inv.BusMinted(ctx); err != nil { + return nil, err + } + if in.BusMemberships, err = inv.BusMemberships(ctx); err != nil { + return nil, err + } + return wouldMake, nil +} + +// readBuilds is the build half of the Input: every repository the mesh built, each module's current build, and the build made +// from a commit of a module for the machines whose node-engine reads them. +func readBuilds(ctx context.Context, open *stores, in *compose.Input, plans map[string]compose.Resolved, shape inputShape) error { + inv := open.inventory + held, err := inv.Held(ctx) + if err != nil { + return err + } + for repository := range held { + in.Built = append(in.Built, repository) + } + sort.Strings(in.Built) + in.CurrentBuilds = make(map[string]string, len(shape.current)) + for module, b := range shape.current { + in.CurrentBuilds[module] = b.Commit + } + // The build made from each commit a declaration may carry, for the machines that read a build per module. + for _, n := range in.Nodes { + if !n.ReadsModules { + continue + } + resolved, ok := plans[n.Name] + if !ok { + continue + } + for _, m := range resolved.Plan.Modules { + for _, commit := range []string{shape.current[m.Module].Commit, shape.kept[n.Name][m.Module], shape.sent[n.Name][m.Module]} { + if commit == "" { + continue + } + if _, done := in.BuildIDs[m.Module][commit]; done { + continue + } + build, err := inv.BuildOf(ctx, m.Module, commit) + if err != nil { + return err + } + if in.BuildIDs == nil { + in.BuildIDs = map[string]map[string]string{} + } + if in.BuildIDs[m.Module] == nil { + in.BuildIDs[m.Module] = map[string]string{} + } + in.BuildIDs[m.Module][commit] = build + } + } + } + return nil +} + +// shelfWith is the shelf with each module a node keeps at the build it runs put in place of the current one: a copy. +func shelfWith(shelf map[string]catalogue.Manifest, kept map[string]catalogue.Manifest) map[string]catalogue.Manifest { + if len(kept) == 0 { + return shelf + } + out := make(map[string]catalogue.Manifest, len(shelf)+len(kept)) + for name, m := range shelf { + out[name] = m + } + for name, m := range kept { + out[name] = m + } + return out +} + +// sharedProviderModule is which module answers a shared credential on the providing node: the one in the plan, else the first +// the catalogue names (providerModuleOf, in an order that does not move between calls). +func sharedProviderModule(plan catalogue.Resolution, shelf map[string]catalogue.Manifest, n catalogue.Needed) string { + for _, m := range plan.Modules { + if _, shared := m.SharedCredentialOf(n.Name); shared { + return m.Module + } + } + names := make([]string, 0, len(shelf)) + for name := range shelf { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + if _, shared := shelf[name].SharedCredentialOf(n.Name); shared { + return name + } + } + return "" +} diff --git a/cmd/mesh-controller/compose_input_credentials_integration_test.go b/cmd/mesh-controller/compose_input_credentials_integration_test.go new file mode 100644 index 00000000..411ea852 --- /dev/null +++ b/cmd/mesh-controller/compose_input_credentials_integration_test.go @@ -0,0 +1,354 @@ +//go:build integration + +package main + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "github.com/novox/mesh-controller/internal/inventory" +) + +// tableState is every table's rows and every sequence's last value, as the adapter's reads must leave them. +func tableState(t *testing.T, inv *inventory.Inventory) map[string]string { + t.Helper() + out := map[string]string{} + for _, row := range inv.QueryForTest(t, `select table_name from information_schema.tables + where table_schema = 'public' and table_type = 'BASE TABLE' order by table_name`) { + name := strings.Trim(row[0], `"`) + count := inv.QueryForTest(t, fmt.Sprintf(`select count(*)::text from %q`, name)) + out["rows:"+name] = fmt.Sprint(count) + digest := inv.QueryForTest(t, fmt.Sprintf(`select coalesce(md5(string_agg(t::text, '|' order by t::text)), '') from %q t`, name)) + out["content:"+name] = fmt.Sprint(digest) + } + for _, row := range inv.QueryForTest(t, `select sequencename, coalesce(last_value::text, '') from pg_sequences + where schemaname = 'public' order by sequencename`) { + out["sequence:"+row[0]] = row[1] + } + return out +} + +// 1382/AC4 and 1383/AC6: building the Input writes nothing. The store's rows, their contents and its sequences are the same +// before and after, on a mesh whose credentials are not made, whose ports are not given and which keeps a recorded build. +func TestBuildingTheInputWritesNothing(t *testing.T) { + open, _ := aMeshWithSecrets(t) + ctx := t.Context() + inv := open.inventory + // A consumer whose credential nothing has made yet, on a mesh that already composed. + register(t, open, catalogue.Manifest{Module: "reports", Version: "1", Requires: []string{"s3-bucket"}, + Resources: []map[string]any{{"id": "config", "type": "file", "path": "/etc/reports/config.json", "mode": "0600", + "content": "{}", "merge": "json"}}}) + if _, err := inv.Assign(ctx, "laptop", "reports"); err != nil { + t.Fatal(err) + } + before := tableState(t, inv) + if _, err := readComposeInput(ctx, open); err != nil { + t.Fatal(err) + } + after := tableState(t, inv) + var moved []string + for k, v := range after { + if before[k] != v { + moved = append(moved, k) + } + } + sort.Strings(moved) + if len(moved) > 0 { + t.Fatalf("building the Input wrote: %v", moved) + } +} + +// a consumer of the s3-bucket aMeshWithSecrets' provider offers, with a file the credential goes into. +func aConsumerOfTheBucket(name string) catalogue.Manifest { + return catalogue.Manifest{Module: name, Version: "1", Requires: []string{"s3-bucket"}, + Secrets: map[string]string{"s3-bucket": "/etc/" + name + "/s3"}, + Resources: []map[string]any{{"id": "config", "type": "file", "path": "/etc/" + name + "/config.json", "mode": "0600", + "content": "{}", "merge": "json"}}} +} + +// 1383/AC1: of a provider's three consumers, two have their credentials made and one has not: Issued holds the two rows, with +// the copy sealed to the provider, the third is a credential the next send makes, and no row was inserted for it (planFor +// inserted it, plan.go: SecretFor in every mode). +func TestAPairCredentialNoRowHoldsIsAWouldMakeAndNoRowIsInserted(t *testing.T) { + open, _ := aMeshWithSecrets(t) + ctx := t.Context() + inv := open.inventory + register(t, open, aConsumerOfTheBucket("reports")) + if _, err := inv.Assign(ctx, "laptop", "reports"); err != nil { + t.Fatal(err) + } + // A send makes the credentials of files and reports; a question makes none. + if _, err := composeSend(ctx, open, "laptop"); err != nil { + t.Fatal(err) + } + register(t, open, aConsumerOfTheBucket("metrics")) + if _, err := inv.Assign(ctx, "laptop", "metrics"); err != nil { + t.Fatal(err) + } + before := tableState(t, inv) + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + var rows []compose.Issued + for _, r := range in.Issued { + if r.Provider == "anchor" && r.Provision == "s3-bucket" { + rows = append(rows, r) + if r.ForProvider == "" { + t.Errorf("the row of %s carries no copy sealed to the provider", r.ConsumerModule) + } + } + } + if len(rows) != 2 { + t.Fatalf("Issued holds %d row(s) for anchor's s3-bucket, want files and reports: %+v", len(rows), rows) + } + var would []string + for _, p := range in.wouldMake { + would = append(would, p.ConsumerModule) + } + if len(would) != 1 || would[0] != "metrics" { + t.Fatalf("the credentials the next send makes are %v, want metrics", would) + } + for _, c := range in.Credentials { + if c.ConsumerModule == "metrics" { + t.Errorf("a credential nobody holds has an entry: %+v", c) + } + } + after := tableState(t, inv) + for k, v := range after { + if before[k] != v { + t.Errorf("reading the Input moved %s", k) + } + } +} + +// 1383/AC2: a credential only a person gives that nobody gave is not given, and so is one given under a key the machine no +// longer has; a credential given by a person for the pair and sealed to a key one end no longer has is refused: in the words +// plan.go and secrets.go use. +func TestACredentialOnlyAPersonGivesIsNotGivenAndAGivenOneUnderAnOldKeyIsSaidSo(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + inv := open.inventory + register(t, open, catalogue.Manifest{Module: "indexers", Version: "1", Provides: []catalogue.Offer{{ + Name: "indexer-api", Scope: catalogue.ScopeMesh, Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}}) + register(t, open, catalogue.Manifest{Module: "grabber", Version: "1", Requires: []string{"indexer-api"}, + Secrets: map[string]string{"indexer-api": "/etc/grabber/key"}}) + register(t, open, aConsumerOfTheBucket("ledger")) + register(t, open, catalogue.Manifest{Module: "objects", Version: "1", Provides: catalogue.FromAnywhere("s3-bucket")}) + for _, a := range [][2]string{{"anchor", "indexers"}, {"laptop", "grabber"}, {"anchor", "objects"}, {"laptop", "ledger"}} { + if _, err := inv.Assign(ctx, a[0], a[1]); err != nil { + t.Fatalf("assign %s %s: %v", a[0], a[1], err) + } + } + stateOf := func(provision, consumerModule string) (compose.Credential, bool) { + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + for _, c := range in.Credentials { + if c.Provision == provision && c.ConsumerModule == consumerModule { + return c, true + } + } + return compose.Credential{}, false + } + + c, found := stateOf("indexer-api", "grabber") + if !found || c.State != compose.CredentialNotGiven || !strings.Contains(c.Why, "nobody gave it") { + t.Fatalf("a credential nobody gave reads as %+v", c) + } + if err := inv.AcceptSecretForPair(ctx, "indexer-api", "laptop", "grabber", "anchor", "", "the-key"); err != nil { + t.Fatal(err) + } + if c, found := stateOf("indexer-api", "grabber"); !found || c.State != compose.CredentialHeld || c.Sealed == "" { + t.Fatalf("a credential a person gave reads as %+v", c) + } + // The consumer's machine generated a key of its own since. + if err := inv.RecordSealingKey(ctx, nodeID(t, open, "laptop"), aPublicKey(t)); err != nil { + t.Fatal(err) + } + if c, found := stateOf("indexer-api", "grabber"); !found || c.State != compose.CredentialNotGiven || + !strings.Contains(c.Why, "a sealing key one end no longer has") { + t.Fatalf("a credential given under a key the machine no longer has reads as %+v", c) + } + + // A credential for a pair, given by a person, under a key one end has since replaced: the mesh cannot make another. + if err := inv.RecordSealingKey(ctx, nodeID(t, open, "laptop"), aPublicKey(t)); err != nil { + t.Fatal(err) + } + if err := inv.AcceptSecretForPair(ctx, "s3-bucket", "laptop", "ledger", "anchor", "", "the-bucket-key"); err != nil { + t.Fatal(err) + } + if err := inv.RecordSealingKey(ctx, nodeID(t, open, "laptop"), aPublicKey(t)); err != nil { + t.Fatal(err) + } + if c, found := stateOf("s3-bucket", "ledger"); !found || c.State != compose.CredentialRefused || + !strings.Contains(c.Why, "was accepted from a person, and a sealing key at one end has changed since") { + t.Fatalf("a credential given for the pair under an old key reads as %+v", c) + } +} + +// 1383/AC3: the own secrets carry their origin, the key they are sealed to and the sealed value; the vault's kept boxes, the +// licences (with their keys given, so an empty key means the mesh holds none), the users the bus has minted and the +// memberships equal what the inventory answers. +func TestTheAdaptersInputCarriesTheOwnSecretsTheBoxesTheLicencesAndTheBus(t *testing.T) { + open, _ := aMeshWithSecrets(t) + ctx := t.Context() + inv := open.inventory + key, err := inv.SealingKeyOf(ctx, "anchor") + if err != nil { + t.Fatal(err) + } + sealed, err := inv.SecretForModule(ctx, "anchor", "objects", "admin") + if err != nil { + t.Fatal(err) + } + box := json.RawMessage(`{"v":1,"eph":"e","iv":"i","tag":"t","ct":"c"}`) + if err := inv.KeepCoreBox(ctx, "anchor", inventory.CoreBox{Module: "objects", Name: "admin", Generation: 4, + SealedTo: key, Box: box}); err != nil { + t.Fatal(err) + } + if err := inv.SeedBusUser(ctx, inventory.BusUser{Username: "anchor.objects", Kind: "module", Node: "anchor", + Module: "objects"}, "a-password"); err != nil { + t.Fatal(err) + } + if err := inv.PutBusMembership(ctx, "laptop", "sealed-membership"); err != nil { + t.Fatal(err) + } + held, err := open.Licences(ctx) + if err != nil { + t.Fatal(err) + } + if err := held.Add(ctx, "claude", "static", map[string]any{"model": "x"}); err != nil { + t.Fatal(err) + } + + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + var own *compose.OwnSecretRow + for i := range in.OwnSecrets { + if r := &in.OwnSecrets[i]; r.Node == "anchor" && r.Module == "objects" && r.Name == "admin" { + own = r + } + } + if own == nil || own.Origin != "made" || own.SealedTo != key || own.Sealed != sealed { + t.Errorf("the own secret reads as %+v; the inventory holds %q sealed to %q", own, sealed, key) + } + var kept *compose.CoreBox + for i := range in.KeptCoreBoxes { + if b := &in.KeptCoreBoxes[i]; b.Node == "anchor" && b.Module == "objects" && b.Name == "admin" { + kept = b + } + } + if kept == nil || kept.Generation != 4 || kept.SealedTo != key || string(kept.Box) != string(box) { + t.Errorf("the kept box reads as %+v", kept) + } + if got := strings.Join(in.BusMinted, ","); !strings.Contains(got, "anchor.objects") { + t.Errorf("BusMinted is %v", in.BusMinted) + } + if in.BusMemberships["laptop"] != "sealed-membership" { + t.Errorf("BusMemberships is %v", in.BusMemberships) + } + if len(in.Licences) != 1 || in.Licences[0].Name != "claude" || in.Licences[0].Keys == nil { + t.Errorf("Licences read as %+v", in.Licences) + } +} + +// 1383/AC4: the bus's user list is what busUserList gives for the machine that runs the bus, and the Input carries none when +// no module of the shelf runs the bus. +func TestTheBusUsersAreWhatTheControllerComposesForTheMachineThatRunsTheBus(t *testing.T) { + open, _ := aMeshWithSecrets(t) + ctx := t.Context() + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + if in.BusUsers != "" || len(in.BusMissing) != 0 { + t.Fatalf("a mesh whose shelf runs no bus carries a bus user list: %q %v", in.BusUsers, in.BusMissing) + } + bus := catalogue.Manifest{Module: "nats", Version: "1", BusUsers: "/etc/nats/users.conf", + Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}} + register(t, open, bus) + if err := open.inventory.SeedBusUser(ctx, inventory.BusUser{Username: "controller", Kind: "controller"}, "pw"); err != nil { + t.Fatal(err) + } + in, err = readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + list, missing, _ := busUserList(ctx, open.inventory, []catalogue.Manifest{bus}) + if in.BusUsers != list || fmt.Sprint(in.BusMissing) != fmt.Sprint(missing) { + t.Errorf("the Input carries %q (%v); busUserList gives %q (%v)", in.BusUsers, in.BusMissing, list, missing) + } +} + +// 1383/AC5: Built, CurrentBuilds and BuildIDs equal the inventory's Held, CurrentBuilds and BuildOf, and the serving key and +// the certificate are the machine's. +func TestTheAdaptersInputCarriesTheBuildsAndTheMachinesKey(t *testing.T) { + open, _ := aMeshWithSecrets(t) + ctx := t.Context() + inv := open.inventory + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + held, err := inv.Held(ctx) + if err != nil { + t.Fatal(err) + } + var wantBuilt []string + for repository := range held { + wantBuilt = append(wantBuilt, repository) + } + sort.Strings(wantBuilt) + if fmt.Sprint(in.Built) != fmt.Sprint(wantBuilt) && !(len(in.Built) == 0 && len(wantBuilt) == 0) { + t.Errorf("Built is %v, the inventory's %v", in.Built, wantBuilt) + } + current, err := inv.CurrentBuilds(ctx) + if err != nil { + t.Fatal(err) + } + for module, b := range current { + if in.CurrentBuilds[module] != b.Commit { + t.Errorf("CurrentBuilds[%s] is %q, the inventory's %q", module, in.CurrentBuilds[module], b.Commit) + } + } + for module, byCommit := range in.BuildIDs { + for commit, build := range byCommit { + if want, _ := inv.BuildOf(ctx, module, commit); want != build { + t.Errorf("BuildIDs[%s][%s] is %q, the inventory's %q", module, commit, build, want) + } + } + } +} + +// The lead's answer 4: where the mesh holds no credential for any bus user, the machine that runs the bus is refused with +// busUserList's words (the controller's composition refused it), the Input still builds, and another machine composes. +func TestTheBusMachineIsRefusedWhereNoUserHasACredentialAndTheOthersCompose(t *testing.T) { + open, _ := aMeshWithSecrets(t) + ctx := t.Context() + bus := catalogue.Manifest{Module: "nats", Version: "1", BusUsers: "/etc/nats/users.conf", + Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}} + register(t, open, bus) + register(t, open, aWebModule()) + if _, err := assign(ctx, open, "anchor", "nats"); err != nil { + t.Fatal(err) + } + if _, err := assign(ctx, open, "laptop", "web"); err != nil { + t.Fatal(err) + } + if _, _, err := composeQuestion(ctx, open, "anchor", compose.Foreseeing, false); err == nil || + !strings.Contains(err.Error(), "not one user has a credential") { + t.Fatalf("the bus machine is composed: %v", err) + } + if _, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false); err != nil { + t.Fatalf("a machine that does not run the bus is refused: %v", err) + } +} diff --git a/cmd/mesh-controller/compose_input_integration_test.go b/cmd/mesh-controller/compose_input_integration_test.go new file mode 100644 index 00000000..6e4f190c --- /dev/null +++ b/cmd/mesh-controller/compose_input_integration_test.go @@ -0,0 +1,329 @@ +//go:build integration + +package main + +import ( + "reflect" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "github.com/novox/mesh-controller/internal/link" +) + +// 1382: the adapter's Input names, for every node, what the inventory answers for it, each compared with the read named +// beside the field in go/compose/input.go (novox/hq ADR 0453 clause 3). +func TestTheAdaptersInputNamesWhatTheInventoryAnswersForEachNode(t *testing.T) { + open, _ := aMeshWithSecrets(t) + ctx := t.Context() + inv := open.inventory + if err := inv.SetPublicDomain(ctx, "anchor", "example.test"); err != nil { + t.Fatal(err) + } + if _, err := inv.PortFor(ctx, "laptop", "files", 8080, false); err != nil { + t.Fatal(err) + } + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + + shelf, err := inv.Catalogue(ctx) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(in.Catalogue, shelf) { + t.Error("the Input's Catalogue is not the shelf the inventory holds") + } + nodes, err := inv.Nodes(ctx) + if err != nil { + t.Fatal(err) + } + if len(in.Nodes) != len(nodes) { + t.Fatalf("the Input names %d node(s), the inventory has %d", len(in.Nodes), len(nodes)) + } + places, err := inv.Overlays(ctx) + if err != nil { + t.Fatal(err) + } + for _, record := range nodes { + n, ok := nodeOf(in.Input, record.Name) + if !ok { + t.Fatalf("%s is not in the Input", record.Name) + } + for _, p := range places { + if p.Name == record.Name { + if want := (compose.Place{Key: p.Key, Endpoint: p.Endpoint, Site: p.Site, Hub: p.Hub, Address: p.Address}); n.Place != want { + t.Errorf("%s: Place is %+v, the inventory's %+v", record.Name, n.Place, want) + } + } + } + assigned, _ := inv.Assigned(ctx, record.Name) + if !reflect.DeepEqual(n.Assigned, assigned) { + t.Errorf("%s: Assigned is %v, the inventory's %v", record.Name, n.Assigned, assigned) + } + caps, _ := inv.ProfileOf(ctx, record.Name) + if !reflect.DeepEqual(n.Capabilities, caps) { + t.Errorf("%s: Capabilities are %v, the inventory's %v", record.Name, n.Capabilities, caps) + } + domain, _ := inv.PublicDomainOf(ctx, record.Name) + if n.PublicDomain != domain { + t.Errorf("%s: PublicDomain is %q, the inventory's %q", record.Name, n.PublicDomain, domain) + } + if n.Account != record.Account || n.AccountHome != record.AccountHome || n.AgentAccount != record.AgentAccount || + n.AgentAccountHome != record.AgentAccountHome || n.Adopted != record.Adopted { + t.Errorf("%s: the accounts or the mode differ from the inventory's: %+v", record.Name, n) + } + pins, _ := inv.PinsFor(ctx, record.Name) + if len(pins) != len(n.Pins) || (len(pins) > 0 && !reflect.DeepEqual(n.Pins, pins)) { + t.Errorf("%s: Pins are %v, the inventory's %v", record.Name, n.Pins, pins) + } + bound, _ := inv.BindingsFor(ctx, record.Name) + if len(bound) != len(n.Bound) || (len(bound) > 0 && !reflect.DeepEqual(n.Bound, bound)) { + t.Errorf("%s: Bound is %v, the inventory's %v", record.Name, n.Bound, bound) + } + key, _ := inv.SealingKeyOf(ctx, record.Name) + if n.SealingKey != key { + t.Errorf("%s: SealingKey is %q, the inventory's %q", record.Name, n.SealingKey, key) + } + ports, _ := inv.PortsFor(ctx, record.Name) + want := map[string]map[int]int{} + for _, a := range ports { + if want[a.Module] == nil { + want[a.Module] = map[int]int{} + } + want[a.Module][a.Wanted] = a.Machine + } + if len(want) != len(n.Ports) || (len(want) > 0 && !reflect.DeepEqual(n.Ports, want)) { + t.Errorf("%s: Ports are %v, the inventory's %v", record.Name, n.Ports, want) + } + // The settings of every module it resolves to, layer for layer. + resolved, err := compose.Resolve(in.Input, record.Name) + if err != nil { + continue + } + for _, m := range resolved.Plan.Modules { + layers, err := inv.SettingsFor(ctx, record.Name, m.Module) + if err != nil { + t.Fatal(err) + } + if len(layers) != len(n.Settings[m.Module]) || (len(layers) > 0 && !reflect.DeepEqual(n.Settings[m.Module], layers)) { + t.Errorf("%s: the settings of %s are %v, the inventory's %v", record.Name, m.Module, n.Settings[m.Module], layers) + } + } + } + if domain := nodeDomain(in.Input, "anchor"); domain != "example.test" { + t.Errorf("anchor's PublicDomain is %q", domain) + } + if got := nodePorts(in.Input, "laptop"); got["files"][8080] == 0 { + t.Errorf("laptop's port for files:8080 is not in the Input: %v", got) + } +} + +func nodeDomain(in compose.Input, name string) string { + n, _ := nodeOf(in, name) + return n.PublicDomain +} + +func nodePorts(in compose.Input, name string) map[string]map[int]int { + n, _ := nodeOf(in, name) + return n.Ports +} + +// 1382/AC2: a node keeping a recorded module at the build it runs has that module in Kept and KeptBuilds, and the Input's +// Catalogue is the current shelf, not the kept manifest (R4a review round 1, finding 1). +func TestANodeKeepingARecordedBuildHasItInKeptAndTheCatalogueStaysCurrent(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + inv := open.inventory + start := time.Now().Add(-time.Hour) + image := "registry.invalid:5000/postgres/server@sha256:" + strings.Repeat("a", 64) + for _, b := range []link.BuildResult{ + aContainerBuild(t, "postgres", "c1111111", catalogue.PolicyRecord, start, map[string][2]string{"server": {image, ""}}), + } { + if _, _, err := takeIn(ctx, inv, asTheOperator(t, inv, b)); err != nil { + t.Fatal(err) + } + } + if _, err := inv.Assign(ctx, "anchor", "postgres"); err != nil { + t.Fatal(err) + } + if err := inv.RecordSent(ctx, nodeID(t, open, "anchor"), "d-anchor", map[string]string{"postgres": "c1111111"}); err != nil { + t.Fatal(err) + } + if _, _, err := takeIn(ctx, inv, aContainerBuild(t, "postgres", "c2222222", catalogue.PolicyRecord, start.Add(time.Minute), + map[string][2]string{"server": {image, "runtime"}})); err != nil { + t.Fatal(err) + } + + // A send that is not a person's push keeps the build the machine runs. + in, err := readComposeInput(keepingRecorded(ctx), open) + if err != nil { + t.Fatal(err) + } + anchor, _ := nodeOf(in.Input, "anchor") + if anchor.KeptBuilds["postgres"] != "c1111111" { + t.Fatalf("anchor keeps %v, want postgres at c1111111", anchor.KeptBuilds) + } + kept, found := anchor.Kept["postgres"] + if !found { + t.Fatalf("the kept manifest of postgres is not in anchor's Kept: %v", anchor.Kept) + } + current, err := inv.Catalogue(ctx) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(in.Catalogue["postgres"], current["postgres"]) { + t.Error("the Input's Catalogue is not the current shelf") + } + if reflect.DeepEqual(kept, current["postgres"]) { + t.Error("the kept manifest is the current one: nothing is kept") + } + if laptop, _ := nodeOf(in.Input, "laptop"); len(laptop.Kept) != 0 || len(laptop.KeptBuilds) != 0 { + t.Errorf("laptop keeps what only anchor runs: %v", laptop.KeptBuilds) + } + + // A person's push keeps nothing. + pushed, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + if a, _ := nodeOf(pushed.Input, "anchor"); len(a.Kept) != 0 || len(a.KeptBuilds) != 0 { + t.Errorf("a person's push keeps %v", a.KeptBuilds) + } +} + +// 1382/AC3: the seats on record, the tunnels, the adopted tunnel, its carried peers and the nodes joining equal the +// inventory's answers; the range and the suffix are read from the environment once, here. +func TestTheAdaptersInputCarriesTheNetworkAsTheInventoryHoldsIt(t *testing.T) { + open, _ := aMeshWithSecrets(t) + ctx := t.Context() + inv := open.inventory + t.Setenv(OverlayCIDRVar, "10.99.0.0/16") + t.Setenv("MESH_INTERNAL_SUFFIX", "mesh.test") + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + holdings, err := inv.Holdings(ctx) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(in.Holdings, holdings) { + t.Errorf("Holdings are %v, the inventory's %v", in.Holdings, holdings) + } + tunnels, err := inv.Tunnels(ctx) + if err != nil { + t.Fatal(err) + } + if len(tunnels) != len(in.Tunnels) { + t.Errorf("Tunnels are %v, the inventory's %v", in.Tunnels, tunnels) + } + if _, hub, _, err := inv.AdoptedTunnel(ctx); err != nil { + t.Fatal(err) + } else if (hub == "") != (in.Adopted == nil) { + t.Errorf("Adopted is %v, the inventory's hub is %q", in.Adopted, hub) + } + carried, err := inv.CarriedPeers(ctx) + if err != nil { + t.Fatal(err) + } + if len(carried) != len(in.CarriedPeers) { + t.Errorf("CarriedPeers are %v, the inventory's %v", in.CarriedPeers, carried) + } + joining, err := inv.NodesWithALiveToken(ctx) + if err != nil { + t.Fatal(err) + } + if len(joining) != len(in.Joining) { + t.Errorf("Joining is %v, the inventory's %v", in.Joining, joining) + } + if in.OverlayCIDR != "10.99.0.0/16" || in.InternalSuffix != "mesh.test" { + t.Errorf("the range and the suffix are %q and %q", in.OverlayCIDR, in.InternalSuffix) + } + // Read once: what the environment says afterwards moves nothing already read. + t.Setenv(OverlayCIDRVar, "10.1.0.0/16") + if in.OverlayCIDR != "10.99.0.0/16" { + t.Error("the range moved with the environment") + } +} + +// The lead's equality finding (build 1792487777757012513): a provider on one machine serves a fact that says ${setting:domain}, +// the setting is set for it, and a consumer on another machine takes that fact. The first pass of the adapter resolved every +// node with no settings in the Input, so the served fact could not be settled and the whole Input failed. The settings are in +// the Input before any node is resolved, and the Input builds, with the same plan for the consumer as the controller's own +// composition. It failed first, with the finding's words. +func TestAServedFactThatSaysASettingIsResolvedWithTheSettingInTheInput(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + register(t, open, catalogue.Manifest{Module: "step-ca", Version: "1", + Provides: []catalogue.Offer{{Name: "acme-ca", Scope: catalogue.ScopeMesh}}, + Serves: map[string]map[string]any{"acme-ca": {"domain": "${setting:domain}"}}}) + register(t, open, catalogue.Manifest{Module: "route-proxy", Version: "1", Requires: []string{"acme-ca"}}) + // The setting is there before the machines are assigned: an assignment resolves the mesh, and a served fact that says + // ${setting:domain} resolves only with it. + if err := open.inventory.KeepSettings(ctx, "", "step-ca", map[string]any{"domain": "example.test"}); err != nil { + t.Fatal(err) + } + if _, err := assign(ctx, open, "anchor", "step-ca"); err != nil { + t.Fatal(err) + } + if _, err := assign(ctx, open, "laptop", "route-proxy"); err != nil { + t.Fatal(err) + } + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatalf("the Input cannot be built: %v", err) + } + if anchor, _ := nodeOf(in.Input, "anchor"); len(anchor.Settings["step-ca"]) == 0 { + t.Fatalf("the Input does not carry the setting of the provider: %v", anchor.Settings) + } + oldPlan, _, err := resolveNode(ctx, open, "laptop") + if err != nil { + t.Fatal(err) + } + newPlan, _, err := resolveNode(withInputCache(ctx), open, "laptop") + if err != nil { + t.Fatalf("the consumer does not resolve: %v", err) + } + if !reflect.DeepEqual(oldPlan.Needs, newPlan.Needs) { + t.Errorf("the consumer's needs are %+v, the controller's own %+v", newPlan.Needs, oldPlan.Needs) + } + t.Logf("a served fact that says a setting resolves with the setting in the Input: the Input builds and the consumer's needs equal planFor's") +} + +// A setting that nothing sets, in a fact a provider serves, is not the adapter's to hide: the controller's own composition +// refuses it as well (theRestOfTheMesh returns servedOnNode's error), and the adapter refuses with the same words rather than +// building an Input that composes a placeholder. +func TestAServedFactThatSaysAnUnsetSettingIsRefusedAsTheControllerRefusesIt(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + register(t, open, catalogue.Manifest{Module: "step-ca", Version: "1", + Provides: []catalogue.Offer{{Name: "acme-ca", Scope: catalogue.ScopeMesh}}, + Serves: map[string]map[string]any{"acme-ca": {"domain": "${setting:domain}"}}}) + register(t, open, catalogue.Manifest{Module: "route-proxy", Version: "1", Requires: []string{"acme-ca"}}) + // Assigned with the setting there, then the setting is cleared: the mesh the controller holds when someone clears it. + if err := open.inventory.KeepSettings(ctx, "", "step-ca", map[string]any{"domain": "example.test"}); err != nil { + t.Fatal(err) + } + if _, err := assign(ctx, open, "anchor", "step-ca"); err != nil { + t.Fatal(err) + } + if _, err := assign(ctx, open, "laptop", "route-proxy"); err != nil { + t.Fatal(err) + } + if err := open.inventory.ClearSettings(ctx, "", "step-ca"); err != nil { + t.Fatal(err) + } + _, _, oldErr := resolveNode(ctx, open, "laptop") + _, newErr := readComposeInput(ctx, open) + if (oldErr == nil) != (newErr == nil) { + t.Fatalf("the controller's composition says %v, the adapter says %v", oldErr, newErr) + } + if oldErr != nil && !strings.Contains(newErr.Error(), "nothing sets \"domain\"") { + t.Errorf("the adapter's words are %q, the controller's %q", newErr, oldErr) + } + t.Logf("an unset setting in a served fact is refused alike: the controller says %v, the adapter says %v", oldErr != nil, newErr != nil) +} diff --git a/cmd/mesh-controller/compose_made_integration_test.go b/cmd/mesh-controller/compose_made_integration_test.go new file mode 100644 index 00000000..ba875ea0 --- /dev/null +++ b/cmd/mesh-controller/compose_made_integration_test.go @@ -0,0 +1,54 @@ +//go:build integration + +package main + +import ( + "context" + "fmt" + "sort" +) + +// made is what a send made in a store, by key and never by value: the machine ports given, the pair credentials and the +// own secrets. +type made struct { + Ports []string + Pairs []string + OwnSecrets []string +} + +// madeIn is what a store holds that a send makes, by key and never by value. +func madeIn(ctx context.Context, open *stores) (made, error) { + var out made + inv := open.inventory + nodes, err := inv.Nodes(ctx) + if err != nil { + return out, err + } + for _, n := range nodes { + ports, err := inv.PortsFor(ctx, n.Name) + if err != nil { + return out, err + } + for _, p := range ports { + out.Ports = append(out.Ports, fmt.Sprintf("%s %s:%d->%d", n.Name, p.Module, p.Wanted, p.Machine)) + } + rows, err := inv.SecretsFrom(ctx, n.Name) + if err != nil { + return out, err + } + for _, s := range rows { + out.Pairs = append(out.Pairs, fmt.Sprintf("%s %s on %s from %s local=%s", s.Name, s.ConsumerModule, s.Consumer, n.Name, s.Local)) + } + } + own, err := inv.OwnSecretRows(ctx) + if err != nil { + return out, err + } + for _, r := range own { + out.OwnSecrets = append(out.OwnSecrets, fmt.Sprintf("%s %s/%s (%s)", r.Node, r.Module, r.Name, r.Origin)) + } + sort.Strings(out.Ports) + sort.Strings(out.Pairs) + sort.Strings(out.OwnSecrets) + return out, nil +} diff --git a/cmd/mesh-controller/compose_once_integration_test.go b/cmd/mesh-controller/compose_once_integration_test.go new file mode 100644 index 00000000..16dd7c4c --- /dev/null +++ b/cmd/mesh-controller/compose_once_integration_test.go @@ -0,0 +1,44 @@ +//go:build integration + +package main + +import ( + "testing" + + "git.novox.be/novox/mesh-sdk/go/compose" +) + +// 1382/AC5: composing every node of one operation builds the Input once; without an operation's cache, once for each. +func TestComposingEveryNodeOfAnOperationBuildsTheInputOnce(t *testing.T) { + open, _ := aMeshWithSecrets(t) + built := 0 + was := composeInputBuilt + composeInputBuilt = func() { built++ } + t.Cleanup(func() { composeInputBuilt = was }) + ctx := withInputCache(t.Context()) + nodes, err := open.inventory.Nodes(ctx) + if err != nil { + t.Fatal(err) + } + for _, n := range nodes { + if _, _, err := resolveNode(ctx, open, n.Name); err != nil && !unresolvable(err) { + t.Fatal(err) + } + if _, _, err := composeQuestion(ctx, open, n.Name, compose.Reading, false); err != nil && !unresolvable(err) { + t.Fatal(err) + } + } + if _, err := generatorsOf(ctx, open); err != nil { + t.Fatal(err) + } + if built != 1 { + t.Fatalf("an operation over %d node(s) built the Input %d time(s)", len(nodes), built) + } + built = 0 + if _, _, err := resolveNode(t.Context(), open, nodes[0].Name); err != nil && !unresolvable(err) { + t.Fatal(err) + } + if built != 1 { + t.Fatalf("a composition with no cache built the Input %d time(s)", built) + } +} diff --git a/cmd/mesh-controller/compose_perform.go b/cmd/mesh-controller/compose_perform.go new file mode 100644 index 00000000..c71c4bfe --- /dev/null +++ b/cmd/mesh-controller/compose_perform.go @@ -0,0 +1,259 @@ +package main + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "slices" + "sort" + "strconv" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "github.com/novox/mesh-controller/internal/inventory" +) + +// perform makes what a composition named foreseen (novox/hq ADR 0453 clause 3, design 47 R4b, issue 1385). +// +// **The composition makes nothing; a send does.** mesh-sdk's composition over an Input names what the next send would make +// (the ports it would assign, the credentials it would seal, the core boxes it would ask the vault for, the seats it would +// pass on, the certificate it would issue) and stands in for each. A send composes, performs, reads the stores again and +// composes again (send in compose_sdk.go), and refuses to send a declaration that still names anything foreseen. A question +// (status, facts, probes, the merge gate's reading) never reaches this file. +// +// What is made is exactly what the controller's own composition made on the way to a declaration it was going to send: +// - every pair credential no row holds, for every machine that resolves, since planning one node resolved them all; +// - the seats the mesh passes on, with the hand act (not on a throwaway mesh, whose hand act has nowhere to go); +// - the machine ports the node's modules were given none for; +// - the node's own secrets, by the controller's own rules (a bus credential nobody issued was refused by the composition); +// - the core credentials, asked of the vault every send, so a rotated one reaches the machine; +// - the certificate of a machine that was never certified. +// +// perform returns whether it made or recorded anything: the reason the caller reads the stores again. +func perform(ctx context.Context, open *stores, in *meshInput, node string, d compose.Declared) (bool, error) { + did := false + inv := open.inventory + + // The seats passed on: recorded in the store with the controller's own hand act (ADR 0449 §4, issue 1317). + if len(d.Passes.Passes) > 0 { + var w io.Writer = os.Stderr + if lw, ok := ctx.Value(leftOutKey{}).(io.Writer); ok { + w = lw + } + for _, line := range d.Passes.Said { + fmt.Fprintln(w, line) + } + if performSeatPasses(ctx, seatPassingOn(inv, w), d.Passes.Passes) { + did = true + } + } + + // Every pair credential the stores hold none for: made once and kept, and sealed to both ends before it was ever written + // down. The error is the one planFor met: the machine that needs it, and why none could be made. + for _, p := range in.wouldMake { + if err := makePair(ctx, inv, p); err != nil { + return did, fmt.Errorf("%s on %s needs %s from %s and no credential could be made for it: %w", + p.ConsumerModule, p.Consumer, p.Name, p.Provider, err) + } + did = true + } + + // The ports this node's modules were given none for. + if len(d.ForeseenPorts) > 0 { + shelf, err := inv.Catalogue(ctx) + if err != nil { + return did, err + } + for _, one := range d.ForeseenPorts { + module, port, ok := strings.Cut(one, ":") + wanted, perr := strconv.Atoi(port) + if !ok || perr != nil { + return did, fmt.Errorf("the composition named a port the controller cannot read: %q", one) + } + fixed := false + for _, l := range nodeShelf(in, shelf, node)[module].Listens { + if l.Port == wanted { + fixed = l.Fixed + } + } + if _, err := inv.PortFor(ctx, node, module, wanted, fixed); err != nil { + return did, fmt.Errorf("%s needs %d reachable on %s and it could not be assigned: %w", module, wanted, node, err) + } + did = true + } + } + + // The node's own secrets: a module's superuser password, an administrator, an account. The composition refused what the + // send refuses (a bus credential nobody issued, a given value under an old key). + for _, one := range d.StoodIn { + module, name, ok := strings.Cut(one, "/") + if !ok || strings.Contains(one, " ") { + continue // a grant's sealed copy, the certificate or the bus's list: made below or by nobody + } + if _, err := inv.SecretForModule(ctx, node, module, name); err != nil { + return did, err + } + did = true + } + + // The certificate of a machine that was never certified: issued and kept, with the authority when there is none yet. + for _, one := range d.StoodIn { + if strings.HasPrefix(one, "the certificate and the authority of ") { + if _, _, err := certificateFor(ctx, open, node); err != nil { + return did, err + } + did = true + break + } + } + return did, nil +} + +// makePair makes one pair credential by the controller's own read of it: the credential for the pair, or the provider's one +// credential sealed to this consumer too. A credential only a person gives is never made (it is not a would-make). +func makePair(ctx context.Context, inv *inventory.Inventory, p inventory.PairCredential) error { + var err error + switch p.Kind { + case inventory.PairShared: + _, err = inv.SharedSecretFor(ctx, p.Name, p.Consumer, p.ConsumerModule, p.Provider, p.ProviderModule, p.Local, p.Own) + case inventory.PairPlain: + _, err = inv.SecretFor(ctx, p.Name, p.Consumer, p.ConsumerModule, p.Provider, p.Local) + default: + return errors.New("a credential only a person gives is not made") + } + if inventory.IsNotGiven(err) { + return nil + } + return err +} + +// stillForeseen says what a composed declaration still stands in for: the lists the composition names, and the stand-in itself +// anywhere in the body (a pair credential's stand-in is not in any list). A declaration that has any is never sent. +func stillForeseen(d compose.Declared) []string { + var left []string + left = append(left, d.Foreseen...) + left = append(left, d.StoodIn...) + for _, p := range d.ForeseenPorts { + left = append(left, "the port "+p) + } + for _, c := range d.CoreBoxesWanted { + left = append(left, "the vault's box for "+c.Module+"/"+c.Name) + } + for _, p := range d.Passes.Passes { + left = append(left, "the pass of "+p.Seat.Name+" to "+p.To.Module+" on "+p.To.Node) + } + if body, err := d.Body(); err == nil { + for _, marker := range []string{catalogue.ForeseenSealed, "asked of the vault by the next send", compose.ForeseenCertificate, + compose.ForeseenBusUsers} { + if strings.Contains(string(body), marker) { + left = append(left, "a stand-in: "+marker) + } + } + } + sort.Strings(left) + return slicesCompact(left) +} + +func slicesCompact(in []string) []string { + var out []string + for _, s := range in { + if len(out) == 0 || out[len(out)-1] != s { + out = append(out, s) + } + } + return out +} + +// nodeShelf is the shelf as one node composes its own set: the current one with each module it keeps at the build it runs put +// in place of the current. +func nodeShelf(in *meshInput, shelf map[string]catalogue.Manifest, node string) map[string]catalogue.Manifest { + n, _ := nodeOf(in.Input, node) + return shelfWith(shelf, n.Kept) +} + +// prepareSend is what a send does before it composes, every send and whatever the composition would have named: the +// controller's own composition did it on the way, so a module's file that names an own secret composes (mesh-sdk's +// composition can stand in for none: a file naming a secret not made yet is refused in every mode), and the core's credentials +// are asked of the vault on every send, so a rotated one reaches the machine and a silent vault is said (core_box.go). +// +// - Each own secret of a module the node runs (not one handed to the provisioner, not one issued outside the mesh, not one a +// person gives) that no row holds is made, unless the composition would refuse it (a bus credential nobody issued): that +// refusal is the composition's to say, in its words. +// - The core's credentials are asked of the vault, the boxes newly answered are kept, and the modules the vault did not answer +// for with no box kept are returned: the controller seals those itself until the vault holds them (ADR 0396). +func prepareSend(ctx context.Context, open *stores, node string) ([]string, error) { + in, err := inputFor(ctx, open) + if err != nil { + return nil, err + } + if in.refused[node] != nil { + return nil, nil // empty-on-error: refused alone for what it keeps; the composition that follows says so, in its words + } + resolved, err := compose.Resolve(in.Input, node) + if err != nil { + return nil, nil // empty-on-error: it does not compose for its own reasons; the composition that follows says them + } + n, _ := nodeOf(in.Input, node) + hands := n.Stated && n.Contract >= compose.HandedContract + handing := catalogue.Rendering{Hands: hands, Provisioner: provisionerAccount(resolved.Plan)} + inv := open.inventory + did := false + if n.SealingKey != "" { + held := map[string]bool{} + for _, r := range in.OwnSecrets { + // Held only when sealed to the key the machine has now, or given by a person (which is not the mesh's + // to make again): a made row under an old key is re-made, as SecretForModule did. + if r.Node == node && (r.SealedTo == n.SealingKey || r.Origin == inventory.OriginAccepted) { + held[r.Module+"/"+r.Name] = true + } + } + for _, m := range resolved.Plan.Modules { + if handing.Handing(m) { + continue + } + plain := m.OwnSecrets.Plain() + names := make([]string, 0, len(plain)) + for name := range plain { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + if plain[name].IssuedBy == catalogue.IssuedOutside || held[m.Module+"/"+name] { + continue + } + switch name { + case "broker": + if !slices.Contains(in.BusMinted, node+"."+m.Module) { + continue + } + case compose.ProvisionerSecret: + if !slices.Contains(in.BusMinted, "provisioner."+node) { + continue + } + } + if _, err := inv.SecretForModule(ctx, node, m.Module, name); err == nil { + did = true + } + } + } + } + var byController []string + if n.Stated && n.Contract >= compose.VaultBoxContract { + boxes, err := coreBoxesFor(ctx, inv, node, resolved.Plan.Modules, func(m catalogue.Manifest) bool { + return handing.Handing(m) + }, true) + if err != nil { + return nil, err + } + did = did || len(boxes.keep) > 0 + byController = boxes.byController + } + if did { + // What was made is in the stores now: the composition reads them afresh. + forgetInput(ctx) + } + return byController, nil +} diff --git a/cmd/mesh-controller/compose_perform_integration_test.go b/cmd/mesh-controller/compose_perform_integration_test.go new file mode 100644 index 00000000..d8aa9bad --- /dev/null +++ b/cmd/mesh-controller/compose_perform_integration_test.go @@ -0,0 +1,235 @@ +//go:build integration + +package main + +import ( + "errors" + "fmt" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "github.com/novox/mesh-controller/internal/link" +) + +func aWebModule() catalogue.Manifest { + return catalogue.Manifest{Module: "web", Version: "1", + Listens: []catalogue.Listening{{Port: 8080, From: catalogue.FromEverywhere}}, + OwnSecrets: catalogue.OwnSecrets{"admin": {Path: "/etc/web/admin"}}, + Resources: []map[string]any{ + {"id": "server", "type": "container", "name": "web", "ports": []any{"8080"}, + "image": "registry.example/web@sha256:" + strings.Repeat("a", 64)}, + }} +} + +// 1385/AC1: a port the composition names foreseen is allocated with the inventory's PortFor for that module and port, a second +// composition names it no more, and the number is the one the controller's own Allocating composition gives in the same store. +func TestPerformAllocatesAForeseenPortAndTheNumberIsTheControllers(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + register(t, open, aWebModule()) + if _, err := assign(ctx, open, "laptop", "web"); err != nil { + t.Fatal(err) + } + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + d, _, err := compose.ComposeIn(in.Input, "laptop", compose.Foreseeing) + if err != nil { + t.Fatal(err) + } + if len(d.ForeseenPorts) != 1 || d.ForeseenPorts[0] != "web:8080" { + t.Fatalf("the composition names the ports %v foreseen, want web:8080", d.ForeseenPorts) + } + did, err := perform(ctx, open, in, "laptop", d) + if err != nil || !did { + t.Fatalf("perform did %v: %v", did, err) + } + held, err := open.inventory.PortsFor(ctx, "laptop") + if err != nil || len(held) != 1 || held[0].Module != "web" || held[0].Wanted != 8080 { + t.Fatalf("the port perform allocated reads %+v: %v", held, err) + } + in, err = readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + if again, _, err := compose.ComposeIn(in.Input, "laptop", compose.Foreseeing); err != nil || len(again.ForeseenPorts) != 0 { + t.Fatalf("a second composition names %v foreseen: %v", again.ForeseenPorts, err) + } + // The number is the one the controller allocates in the same store. + plan, settings, err := resolveNode(ctx, open, "laptop") + if err != nil { + t.Fatal(err) + } + old, err := declarationFor(ctx, open, "laptop", plan, settings) + if err != nil { + t.Fatal(err) + } + after, _, err := compose.ComposeIn(in.Input, "laptop", compose.Foreseeing) + if err != nil { + t.Fatal(err) + } + if got, want := portsOf(after.Resources), portsOf(old.Resources); got != want { + t.Fatalf("the declaration publishes %s; the controller's allocation publishes %s", got, want) + } +} + +func portsOf(resources []map[string]any) string { + var out []string + for _, r := range resources { + if r["type"] == "container" { + out = append(out, fmt.Sprint(r["name"], r["ports"])) + } + } + return strings.Join(out, ";") +} + +// 1385/AC2: an own secret named foreseen is made with SecretForModule, and a pair credential the Input names would-make is +// made as planFor chose; both exist after the send's composition and a second composition names neither. +func TestPerformMakesTheOwnSecretsAndThePairCredentialsTheCompositionNamed(t *testing.T) { + open, _ := aMeshWithSecrets(t) + ctx := t.Context() + inv := open.inventory + register(t, open, aWebModule()) + register(t, open, aConsumerOfTheBucket("reports")) + for _, m := range []string{"web", "reports"} { + if _, err := assign(ctx, open, "laptop", m); err != nil { + t.Fatal(err) + } + } + before, err := madeIn(ctx, open) + if err != nil { + t.Fatal(err) + } + declared, err := composeSend(ctx, open, "laptop") + if err != nil { + t.Fatalf("the send is refused: %v", err) + } + if len(declared.foreseen) != 0 { + t.Fatalf("the declaration still names %v foreseen", declared.foreseen) + } + after, err := madeIn(ctx, open) + if err != nil { + t.Fatal(err) + } + madeNow := func(have, was []string) []string { + seen := map[string]bool{} + for _, s := range was { + seen[s] = true + } + var out []string + for _, s := range have { + if !seen[s] { + out = append(out, s) + } + } + return out + } + if got := madeNow(after.OwnSecrets, before.OwnSecrets); len(got) != 1 || !strings.Contains(got[0], "web/admin") { + t.Errorf("the own secrets made are %v, want laptop web/admin", got) + } + if got := madeNow(after.Pairs, before.Pairs); len(got) != 1 || !strings.Contains(got[0], "reports") { + t.Errorf("the pair credentials made are %v, want the one of reports", got) + } + // And the credential is the one a second composition reads as held. + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + if len(in.wouldMake) != 0 { + t.Errorf("after the send the next send would still make %v", in.wouldMake) + } + if _, err := inv.SecretsFrom(ctx, "anchor"); err != nil { + t.Fatal(err) + } +} + +// 1385/AC3 (the lead's answer 1): with the vault unreachable and no box of the credential kept, the send carries the +// controller's own seal for it (ADR 0396), says so as a condition, and keeps no box. +func TestAVaultThatIsSilentSealsTheSendByTheControllerWhereNoBoxIsKept(t *testing.T) { + open, vault := aCoreMesh(t, link.VaultBoxContract) + vault.silent = errors.New("no responders") + declared, err := composeSend(t.Context(), open, "laptop") + if err != nil { + t.Fatalf("the send is refused: %v", err) + } + f := clientsBroker(declared.Resources) + if f == nil || f["sealed"] == nil || f[catalogue.VaultSealedField] != nil { + t.Fatalf("the core credential is composed as %v: the controller's own seal, until the vault holds it", f) + } + if len(declared.foreseen) != 0 { + t.Fatalf("the declaration still names %v foreseen", declared.foreseen) + } + if c, found := vaultSilence(t, "laptop"); !found || !strings.Contains(c.Summary, "sealed by the controller until the vault holds it") { + t.Fatalf("no condition says why: %+v", c) + } + kept, err := open.inventory.KeptCoreBoxes(t.Context(), "laptop") + if err != nil { + t.Fatal(err) + } + if len(kept["mesh-provisioner-client"]) != 0 { + t.Fatalf("a box was kept for a credential the vault never boxed: %v", kept) + } +} + +// 1385/AC6: perform is idempotent: a second send's composition over the stores it left makes and records nothing. +func TestAFinishedSendMakesNothingTheSecondTime(t *testing.T) { + open, _ := aMeshWithSecrets(t) + ctx := t.Context() + register(t, open, aWebModule()) + if _, err := assign(ctx, open, "laptop", "web"); err != nil { + t.Fatal(err) + } + if _, err := composeSend(ctx, open, "laptop"); err != nil { + t.Fatal(err) + } + before := tableState(t, open.inventory) + if _, err := composeSend(ctx, open, "laptop"); err != nil { + t.Fatal(err) + } + for k, v := range tableState(t, open.inventory) { + if before[k] != v { + t.Errorf("a second send of the same machine moved %s", k) + } + } +} + +// Review round 1, finding 1: a machine that rejoined with a new sealing key keeps its made rows sealed to the old key. A +// send re-makes them for the new key, as SecretForModule did on main, and the file naming the secret is not refused. +func TestASendReMakesAnOwnSecretSealedToAKeyTheMachineNoLongerHas(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + inv := open.inventory + register(t, open, aBot("bot", false)) + if _, err := assign(ctx, open, "laptop", "bot"); err != nil { + t.Fatal(err) + } + if _, err := composeSend(ctx, open, "laptop"); err != nil { + t.Fatalf("the first send is refused: %v", err) + } + if err := inv.RecordSealingKey(ctx, nodeID(t, open, "laptop"), aPublicKey(t)); err != nil { + t.Fatal(err) + } + if _, err := composeSend(ctx, open, "laptop"); err != nil { + t.Fatalf("a machine that rejoined cannot be sent to: %v", err) + } + in, err := readComposeInput(ctx, open) + if err != nil { + t.Fatal(err) + } + n, _ := nodeOf(in.Input, "laptop") + found := false + for _, r := range in.OwnSecrets { + if r.Node == "laptop" && r.Module == "bot" && r.Name == "token" { + found = true + if r.SealedTo != n.SealingKey { + t.Fatalf("the own secret is still sealed to a key the machine no longer has") + } + } + } + if !found { + t.Fatal("the own secret has no row") + } +} diff --git a/cmd/mesh-controller/compose_sdk.go b/cmd/mesh-controller/compose_sdk.go new file mode 100644 index 00000000..56653cf5 --- /dev/null +++ b/cmd/mesh-controller/compose_sdk.go @@ -0,0 +1,500 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "os" + "slices" + "sort" + "strings" + "sync" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "git.novox.be/novox/mesh-sdk/go/validate" + "github.com/novox/mesh-controller/internal/inventory" +) + +// The controller's composition on mesh-sdk's (novox/hq ADR 0453 clause 3, design 47 R4b, issues 1386 to 1388): every +// caller that planned or composed a node reads the Input once for its operation (compose_input.go), asks the SDK to +// compose over it, and, when it sends, performs what the composition names foreseen (compose_perform.go) and composes again. + +// inputCacheKey carries an operation's Input in its context, so the planning of every node of one verb reads the stores once. +type inputCacheKey struct{} + +type inputCache struct { + mu sync.Mutex + by map[string]*meshInput +} + +// withInputCache is ctx for an operation: the Input read for it is kept until a send changes what it read. A context that +// carries none reads the stores for each composition, which is correct and slow. +func withInputCache(ctx context.Context) context.Context { + if _, ok := ctx.Value(inputCacheKey{}).(*inputCache); ok { + return ctx + } + return context.WithValue(ctx, inputCacheKey{}, &inputCache{by: map[string]*meshInput{}}) +} + +// freshInput is ctx for one composition whose Input is read afresh and kept only until the composition makes something: a +// send that is composed after another one made and recorded things (a push's machines, one after the other). +func freshInput(ctx context.Context) context.Context { + return context.WithValue(ctx, inputCacheKey{}, &inputCache{by: map[string]*meshInput{}}) +} + +// keptSignature is what the Input read depends on in ctx: which recorded modules each machine keeps at the build it runs +// (recorded_kept.go), so an Input read for a person's push is never taken for one read for a gated send. +func keptSignature(ctx context.Context) string { + skip, on := keptExcept(ctx) + var names []string + for m := range skip { + names = append(names, m) + } + sort.Strings(names) + every, _ := ctx.Value(keepEveryBuildKey{}).(string) + settings, _ := ctx.Value(keepForSettingsKey{}).(string) + return fmt.Sprintf("kept=%v;except=%v;every=%s;settings=%s", on, names, every, settings) +} + +// inputFor is the Input for ctx's operation. +func inputFor(ctx context.Context, open *stores) (*meshInput, error) { + cache, ok := ctx.Value(inputCacheKey{}).(*inputCache) + if !ok { + return readComposeInput(ctx, open) + } + sig := keptSignature(ctx) + cache.mu.Lock() + defer cache.mu.Unlock() + if in := cache.by[sig]; in != nil { + return in, nil + } + in, err := readComposeInput(ctx, open) + if err != nil { + return nil, err + } + cache.by[sig] = in + return in, nil +} + +// forgetInput drops what the operation read, because a send made or recorded something it read. +func forgetInput(ctx context.Context) { + if cache, ok := ctx.Value(inputCacheKey{}).(*inputCache); ok { + cache.mu.Lock() + cache.by = map[string]*meshInput{} + cache.mu.Unlock() + } +} + +// resolveNode works out everything a node should run, from what was assigned to it (planFor on the SDK): over the Input, +// writing nothing. A failure to compose the node's own modules is marked notResolvable, as planFor marked it; every other +// failure is returned as it is. +func resolveNode(ctx context.Context, open *stores, node string) (catalogue.Resolution, catalogue.SettingsBy, error) { + in, err := inputFor(ctx, open) + if err != nil { + return catalogue.Resolution{}, nil, err + } + if err := in.refused[node]; err != nil { + return catalogue.Resolution{}, nil, err + } + resolved, err := compose.Resolve(in.Input, node) + if err != nil { + if compose.IsUnresolvable(err) { + return catalogue.Resolution{}, nil, notResolvable{err} + } + return catalogue.Resolution{}, nil, err + } + logUnheld(node, resolved.Plan.Unheld) + return resolved.Plan, resolved.Settings, nil +} + +// generatorsOf is what every node's declaration is composed with, over the whole mesh (generators on the SDK). +func generatorsOf(ctx context.Context, open *stores) (map[string]catalogue.Generator, error) { + in, err := inputFor(ctx, open) + if err != nil { + return nil, err + } + return compose.Generators(in.Input) +} + +// renderingOf is what a machine is rendered with as a question reads it (nothing made), with the private network: what the +// filter's rules, the roster and the like are derived from. +func renderingOf(ctx context.Context, open *stores, node string) (catalogue.Rendering, error) { + in, err := inputFor(ctx, open) + if err != nil { + return catalogue.Rendering{}, err + } + rendered, err := compose.Render(in.Input, node, compose.Reading) + if err != nil { + return catalogue.Rendering{}, markUnresolvable(err) + } + with := rendered.Rendering + if with.Generators, err = compose.Generators(in.Input); err != nil { + return catalogue.Rendering{}, err + } + return with, nil +} + +// whoResolvesOn is the machines whose resolution answers a requirement, and why the others did not (whoResolves over the +// Input): by what a module provides, not by its name. A machine whose set does not compose is in refused, said; one the mesh +// could not be asked about is an error, never a refusal (novox/hq 04-ISSUES/152). +func whoResolvesOn(ctx context.Context, open *stores, requirement string) (map[string]bool, map[string]string, error) { + in, err := inputFor(ctx, open) + if err != nil { + return nil, nil, err + } + on := map[string]bool{} + refused := map[string]string{} + for _, n := range in.sortedNodes() { + if err := in.refused[n.Name]; err != nil { + return nil, nil, fmt.Errorf("whether %s answers %q cannot be read: %w", n.Name, requirement, err) + } + resolved, err := compose.Resolve(in.Input, n.Name) + switch { + case compose.IsUnresolvable(err): + refused[n.Name] = err.Error() + continue + case err != nil: + return nil, nil, fmt.Errorf("whether %s answers %q cannot be read: %w", n.Name, requirement, err) + } + for _, m := range resolved.Plan.Modules { + for _, offered := range m.Offers() { + if offered == requirement { + on[n.Name] = true + } + } + } + } + return on, refused, nil +} + +// sortedNodes is the Input's nodes by name, the order the controller reads them in. +func (in *meshInput) sortedNodes() []compose.Node { + out := append([]compose.Node(nil), in.Nodes...) + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) + return out +} + +// composeQuestion composes a node's declaration as a question asks it: nothing is made, and what a send would make is a +// stand-in (Foreseeing) or left out (Reading). It also answers what the node-engine's validator refuses of it. The lines the +// composition found are said where the controller says its own (standard error, or the writer an act gave). +// +// withOrder says the declaration carries the order it was last sent under (composedAndValidated's, which a comparison with the +// last send needs); without it the order is none, as declarationWith left it for the sender to stamp. +func composeQuestion(ctx context.Context, open *stores, node string, mode compose.Mode, withOrder bool) (sendable, []string, error) { + in, err := inputFor(ctx, open) + if err != nil { + return sendable{}, nil, err + } + if err := in.refused[node]; err != nil { + return sendable{}, nil, err + } + d, problems, err := compose.ComposeIn(in.Input, node, mode) + if err != nil { + return sendable{}, nil, markUnresolvable(err) + } + if err := refusedBus(in, node); err != nil { + return sendable{}, nil, err + } + sayFound(ctx, d) + out := sendableOf(d) + if !withOrder { + out.Sequence, out.Epoch, out.Generation = 0, 0, 0 + } + return out, problems, nil +} + +// refusedBus is why the node cannot be composed because it runs the bus and the bus's user list cannot be: no user has a +// credential, so the list would refuse every connection in the mesh (busUserList). The controller's own composition refused that +// machine with those words; no other machine is refused for it. nil when nothing refuses the node. +func refusedBus(in *meshInput, node string) error { + if in.busRefused == nil { + return nil + } + resolved, err := compose.Resolve(in.Input, node) + if err != nil { + return nil // it does not compose for its own reasons, which are said + } + for _, m := range resolved.Plan.Modules { + if m.BusUsers != "" && m.ClaimsSeat("mesh-broker") { + return in.busRefused + } + } + return nil +} + +// markUnresolvable is an error of a node whose own set does not compose, as planFor marked it. +func markUnresolvable(err error) error { + if compose.IsUnresolvable(err) { + return notResolvable{err} + } + return err +} + +// composeSend composes a node's declaration to send it, as the Allocating composition did: composed over what is read, +// then what it named foreseen is made (perform), the stores are read again and it is composed again. A declaration that +// still names anything foreseen is refused, said, and never sent. +func composeSend(ctx context.Context, open *stores, node string) (sendable, error) { + declared, _, err := composeSendValidated(ctx, open, node) + return declared, err +} + +// composeSendValidated is composeSend and what the node-engine's validator refuses of the declaration it composed, for the +// merge gate's two passes (composedAndValidated in its Allocating mode). +func composeSendValidated(ctx context.Context, open *stores, node string) (sendable, []string, error) { + // The core modules whose credentials the controller sealed itself because the vault was silent and no box was kept + // (ADR 0396): the composition stands in a vault box for each, and a send carries the controller's own seal instead. + byController, err := prepareSend(ctx, open, node) + if err != nil { + return sendable{}, nil, err + } + for attempt := 0; ; attempt++ { + in, err := inputFor(ctx, open) + if err != nil { + return sendable{}, nil, err + } + if err := in.refused[node]; err != nil { + return sendable{}, nil, err + } + d, problems, err := compose.ComposeIn(in.Input, node, compose.Foreseeing) + if err != nil { + return sendable{}, nil, markUnresolvable(err) + } + if err := refusedBus(in, node); err != nil { + return sendable{}, nil, err + } + if len(byController) > 0 { + if d, problems, err = sealedByTheController(in, node, d, byController); err != nil { + return sendable{}, nil, err + } + } + left := stillForeseen(d) + resolved, rerr := compose.Resolve(in.Input, node) + if rerr != nil { + return sendable{}, nil, markUnresolvable(rerr) + } + if len(resolved.Foreseen) > 0 { + left = append(left, resolved.Foreseen...) + } + if len(left) == 0 && len(in.wouldMake) == 0 { + sayFound(ctx, d) + out := sendableOf(d) + // The sender stamps the order it allots (order.stamp); a composition for a send carries none of its own. + out.Sequence, out.Epoch, out.Generation = 0, 0, 0 + here, _ := nodeOf(in.Input, node) + out.Builds = buildsOf(in.Input, here, resolved.Plan, d.LeftOutWhy) + out.Bindings = boundToData(resolved.Plan, d.LeftOutWhy) + if len(d.Filled) > 0 { + if out.plans, err = handedPlans(in, node, resolved); err != nil { + return sendable{}, nil, err + } + } + return out, problems, nil + } + if attempt > 0 { + return sendable{}, nil, fmt.Errorf("%s was composed twice and still names what only a send makes (%s): nothing is sent "+ + "that stands in for a value", node, strings.Join(left, "; ")) + } + if _, err := perform(ctx, open, in, node, d); err != nil { + return sendable{}, nil, err + } + // What was made is in the stores now: the next read is a fresh one. + forgetInput(ctx) + } +} + +// restOfTheMesh is what every other machine holds and offers (theRestOfTheMesh over the Input), for a question: the lines it +// finds are said where the controller says its own, and a seat it would pass on is passed by the next send (perform), not by +// the question. shelf, when given, replaces the Input's catalogue: a caller composing a change's shelf. +func restOfTheMesh(ctx context.Context, open *stores, shelf map[string]catalogue.Manifest, exclude string) (catalogue.World, error) { + in, err := inputFor(ctx, open) + if err != nil { + return catalogue.World{}, err + } + input := in.Input + if shelf != nil { + input.Catalogue = shelf + } + world, passes, left, err := compose.TheRestOfTheMesh(input, exclude) + if err != nil { + return catalogue.World{}, err + } + var w io.Writer = os.Stderr + if lw, ok := ctx.Value(leftOutKey{}).(io.Writer); ok { + w = lw + } + for _, l := range left { + fmt.Fprintln(w, l.String()) + } + for _, line := range passes.Said { + fmt.Fprintln(w, line) + } + return world, nil +} + +// restOfTheMeshOf is restOfTheMesh for a caller that holds the inventory alone: the other stores the Input reads are opened for +// the call and let go of again. +func restOfTheMeshOf(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest, exclude string) (catalogue.World, error) { + open := &stores{inventory: inv} + defer func() { + if open.licences != nil { + open.licences.Close() + } + if open.identity != nil { + open.identity.Close() + } + }() + return restOfTheMesh(ctx, open, shelf, exclude) +} + +// sayFound says the lines a composition found while composing, once each: a machine left out of the rest of the mesh, a seat +// that two claimants could take, the bus's users left out for want of a credential. +func sayFound(ctx context.Context, d compose.Declared) { + if len(d.Said) == 0 { + return + } + var w io.Writer = os.Stderr + if lw, ok := ctx.Value(leftOutKey{}).(io.Writer); ok { + w = lw + } + for _, line := range d.Said { + fmt.Fprintln(w, line) + } +} + +// sendableOf is a composed declaration as the controller's sends know it. +func sendableOf(d compose.Declared) sendable { + out := sendable{Resources: d.Resources, Sequence: d.Sequence, Epoch: d.Epoch, Generation: d.Generation, + ReadsModules: d.ReadsModules, moduleOf: d.ModuleOf, buildIDs: d.BuildIDs, modules: d.Modules, + localNetwork: d.LocalNetwork, Received: d.Received, Mesh: d.Mesh, BusUsers: d.BusUsers, + LeftOut: d.LeftOut, leftOutWhy: d.LeftOutWhy, withheld: d.Withheld, unbound: d.Unbound, foreseen: d.Foreseen, + unplaced: d.Unplaced, handedOnly: d.HandedOnly, Filled: d.Filled, Provisioner: d.Provisioner, + requiredBy: d.RequiredBy} + if d.Adoption != nil { + out.Adoption = &adoptionEnvelope{Taken: d.Adoption.Taken, Untaken: d.Adoption.Untaken} + } + return out +} + +// buildsOf is the build of each module a declaration for the node carries, as a send records it (novox/hq issue 259): the +// module's current build for each module in it, the build it was last sent for one left out of it, and a module kept at the +// build the machine runs carried at that one (ADR 0245). Never nil. +func buildsOf(in compose.Input, node compose.Node, plan catalogue.Resolution, leftOut map[string]string) map[string]string { + before := node.SentBuilds + if !node.SentKnown { + before = nil + } + out := map[string]string{} + for _, m := range plan.Modules { + if _, left := leftOut[m.Module]; left { + if was, kept := before[m.Module]; kept { + out[m.Module] = was + } + continue + } + out[m.Module] = in.CurrentBuilds[m.Module] + } + for m, was := range node.KeptBuilds { + if _, carried := out[m]; carried { + out[m] = was + } + } + return out +} + +// handedPlans is the plan of each module handed to the provisioner, which the assignments mirror keeps with the send (design +// 57 P6): the composition's declaration does not carry it, so it is composed from the same rendering again. +func handedPlans(in *meshInput, node string, resolved compose.Resolved) (map[string]catalogue.Plan, error) { + rendered, err := compose.Render(in.Input, node, compose.Foreseeing) + if err != nil { + return nil, err + } + gens, err := compose.Generators(in.Input) + if err != nil { + return nil, err + } + with := rendered.Rendering + with.Generators = gens + composed, err := resolved.Plan.Compose(with) + if err != nil { + return nil, err + } + return composed.Plans, nil +} + +// sealedByTheController is the declaration with the controller's own seal in place of the vault's box for each core module the +// vault did not answer for and no box was kept for (core_box.go, ADR 0396): the file carries "sealed", as the controller's +// composition sent it, until the vault holds the credential. What the composition stood in for the box is replaced and no longer +// wanted; the validator is run again over the body. +func sealedByTheController(in *meshInput, node string, d compose.Declared, modules []string) (compose.Declared, []string, error) { + rendered, err := compose.Render(in.Input, node, compose.Foreseeing) + if err != nil { + return d, nil, err + } + stood, _ := compactBox(catalogue.ForeseenVaultBox) + var wanted []compose.CoreBoxWanted + var foreseen []string + for _, w := range d.CoreBoxesWanted { + if !slices.Contains(modules, w.Module) { + wanted = append(wanted, w) + continue + } + sealed := rendered.Rendering.Needed[w.Module][w.Name] + if sealed == "" { + return d, nil, fmt.Errorf("%s/%s on %s has no seal of the controller's to carry until the vault holds it", w.Module, w.Name, node) + } + for _, r := range d.Resources { + box, isBox := r[catalogue.VaultSealedField] + // By the resource's id alone: its path is the manifest's with the machine's words filled in, and so is not the + // manifest's own. + if !isBox || r["id"] != w.Module+"."+catalogue.NeedID(w.Name) { + continue + } + if had, err := compactBox(box); err != nil || had != stood { + continue + } + delete(r, catalogue.VaultSealedField) + r["sealed"] = sealed + } + } + // A stand-in box that is left is said by its resource, never sent and never guessed at. + for _, r := range d.Resources { + if box, has := r[catalogue.VaultSealedField]; has { + if had, err := compactBox(box); err == nil && had == stood { + if id, _ := r["id"].(string); slices.ContainsFunc(modules, func(m string) bool { return strings.HasPrefix(id, m+".") }) { + return d, nil, fmt.Errorf("%s on %s still carries a stand-in for the vault's box, and the controller has "+ + "no seal of its own for it", id, node) + } + } + } + } + d.CoreBoxesWanted = wanted + for _, f := range d.Foreseen { + module, _, _ := strings.Cut(f, "/") + if !slices.Contains(modules, module) { + foreseen = append(foreseen, f) + } + } + d.Foreseen = foreseen + body, err := d.Body() + if err != nil { + return d, nil, err + } + return d, validate.Declaration(body), nil +} + +// compactBox is a value as the compact JSON it marshals to, so a box is compared by what it says and not by the type that +// holds it. +func compactBox(v any) (string, error) { + raw, err := json.Marshal(v) + if err != nil { + return "", err + } + var out bytes.Buffer + if err := json.Compact(&out, raw); err != nil { + return "", err + } + return out.String(), nil +} diff --git a/cmd/mesh-controller/compose_sdk_test.go b/cmd/mesh-controller/compose_sdk_test.go new file mode 100644 index 00000000..8d6a3cb8 --- /dev/null +++ b/cmd/mesh-controller/compose_sdk_test.go @@ -0,0 +1,111 @@ +package main + +import ( + "context" + "reflect" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" +) + +// A declaration as the SDK composes it is the one the controller sends, byte for byte, whatever it carries: the body is the +// envelope's only marshaller on either side (design 47 R4b, issue 1386). +func TestASendableIsTheComposedDeclarationByteForByte(t *testing.T) { + d := compose.Declared{ + Resources: []map[string]any{{"id": "files.config", "type": "file"}, {"id": "mesh.hosts", "type": "file"}}, + Sequence: 7, Epoch: 3, Generation: 9, ReadsModules: true, + ModuleOf: map[string]string{"files.config": "files"}, BuildIDs: map[string]string{"files": "b-1"}, + Adoption: &compose.Adoption{Taken: []string{"files"}, Untaken: map[string][]string{"nats": {"nats.server"}}}, + Filled: []string{"/etc/files/filled.json"}, Provisioner: "mesh-provisioner", + LeftOut: []string{"cache"}, LeftOutWhy: map[string]string{"cache": "a setting cannot compose"}, + } + want, err := d.Body() + if err != nil { + t.Fatal(err) + } + got, err := sendableOf(d).Body() + if err != nil { + t.Fatal(err) + } + if string(got) != string(want) { + t.Fatalf("the controller would send\n%s\nand the composition made\n%s", got, want) + } + if empty, err := sendableOf(compose.Declared{}).Body(); err != nil || !strings.Contains(string(empty), `"owns_nothing":true`) { + t.Fatalf("an empty declaration is not deliberate: %s %v", empty, err) + } +} + +// 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) { + plan := catalogue.Resolution{Modules: []catalogue.Manifest{{Module: "postgres"}, {Module: "files"}, {Module: "cache"}}} + leftOut := map[string]string{"cache": "a setting cannot compose"} + for name, c := range map[string]struct { + sent map[string]string + known bool + kept map[string]string + want map[string]string + }{ + "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"}, want: map[string]string{"postgres": "p1", "files": "f2", "cache": "c1"}}, + "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"}}, + } { + 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} + if got := buildsOf(in, node, plan, leftOut); !reflect.DeepEqual(got, c.want) { + t.Errorf("%s: a send records %v, want %v", name, got, c.want) + } + } +} + +// 1385: a declaration that stands in for a value anywhere is still foreseen, in a list the composition names or in the body +// alone (a pair credential's stand-in is in no list); one that stands in for nothing is not. It failed first, before the body +// was scanned. +func TestADeclarationThatStandsInForAValueIsStillForeseen(t *testing.T) { + clean := compose.Declared{Resources: []map[string]any{{"id": "files.config", "type": "file", "content": "ok"}}} + if left := stillForeseen(clean); len(left) != 0 { + t.Fatalf("a declaration that stands in for nothing is foreseen: %v", left) + } + for name, d := range map[string]compose.Declared{ + "an own secret": {StoodIn: []string{"files/admin"}}, + "a port": {ForeseenPorts: []string{"files:80"}}, + "a core box": {CoreBoxesWanted: []compose.CoreBoxWanted{{Node: "anchor", Module: "mesh-vault", Name: "root"}}}, + "a pass of a seat": {Passes: compose.Passes{Passes: []catalogue.SeatPass{{Seat: catalogue.Seat{Name: "mesh-store"}}}}}, + "a pair credential": {Resources: []map[string]any{{"id": "files.needs-db", "sealed": catalogue.ForeseenSealed}}}, + "the certificate": {Resources: []map[string]any{{"id": "proxy.cert", "content": compose.ForeseenCertificate}}}, + "the bus's list": {Resources: []map[string]any{{"id": "nats.users", "content": compose.ForeseenBusUsers}}}, + "the vault's box": {Resources: []map[string]any{{"id": "x", "box": catalogue.ForeseenVaultBox}}}, + } { + if left := stillForeseen(d); len(left) == 0 { + t.Errorf("%s: a declaration that stands in for it is not foreseen", name) + } + } +} + +// 1386: an Input read for a person's push is never the one read for a send that keeps recorded builds, and the cache is +// emptied by what a send made. +func TestAnInputIsCachedPerWhatItKeepsAndForgottenAfterASend(t *testing.T) { + ctx := withInputCache(context.Background()) + plain := keptSignature(ctx) + kept := keptSignature(keepingRecorded(ctx)) + bus := keptSignature(keepingRecorded(ctx, "nats")) + every := keptSignature(keepingEveryBuild(ctx, "anchor")) + for name, sig := range map[string]string{"a push": plain, "a kept send": kept, "the bus step": bus, "the grants step": every} { + for other, o := range map[string]string{"a push": plain, "a kept send": kept, "the bus step": bus, "the grants step": every} { + if name != other && sig == o { + t.Errorf("%s and %s read the same Input", name, other) + } + } + } + cache := ctx.Value(inputCacheKey{}).(*inputCache) + cache.by[plain] = &meshInput{} + forgetInput(ctx) + if len(cache.by) != 0 { + t.Fatal("what a send made left the Input it read in the cache") + } +} diff --git a/cmd/mesh-controller/composed_member_check_test.go b/cmd/mesh-controller/composed_member_check_test.go index f5bbc15b..7943412f 100644 --- a/cmd/mesh-controller/composed_member_check_test.go +++ b/cmd/mesh-controller/composed_member_check_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/composed_verdict_test.go b/cmd/mesh-controller/composed_verdict_test.go index a8573833..f96bc00c 100644 --- a/cmd/mesh-controller/composed_verdict_test.go +++ b/cmd/mesh-controller/composed_verdict_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/config_move.go b/cmd/mesh-controller/config_move.go index 1b65621d..8052fae6 100644 --- a/cmd/mesh-controller/config_move.go +++ b/cmd/mesh-controller/config_move.go @@ -15,8 +15,8 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/config_moved_integration_test.go b/cmd/mesh-controller/config_moved_integration_test.go index 7c930d12..f7a5b5cb 100644 --- a/cmd/mesh-controller/config_moved_integration_test.go +++ b/cmd/mesh-controller/config_moved_integration_test.go @@ -13,8 +13,8 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" - "github.com/novox/mesh-controller/internal/overlay" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/overlay" ) func aConfiguredMesh(t *testing.T) (*stores, configuredMesh, map[string]map[string]any) { diff --git a/cmd/mesh-controller/config_moved_test.go b/cmd/mesh-controller/config_moved_test.go index 390a51e7..7aa2aa8c 100644 --- a/cmd/mesh-controller/config_moved_test.go +++ b/cmd/mesh-controller/config_moved_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" - snapshot "github.com/novox/mesh-controller/internal/facts" + "git.novox.be/novox/mesh-sdk/go/catalogue" + snapshot "git.novox.be/novox/mesh-sdk/go/facts" "github.com/novox/mesh-controller/internal/link" ) @@ -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 { t.Helper() ctx := t.Context() - gens, err := generators(ctx, open) - if err != nil { - t.Fatal(err) - } shelf, err := open.inventory.Catalogue(ctx) if err != nil { t.Fatal(err) } told := map[string]string{} for _, node := range fourMachines { - plan, settings, err := planFor(ctx, open, node) + plan, _, err := resolveNode(ctx, open, node) if err != nil { 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 { t.Fatalf("%s: %v", node, err) } diff --git a/cmd/mesh-controller/core_box.go b/cmd/mesh-controller/core_box.go index e3d1ac13..33c99192 100644 --- a/cmd/mesh-controller/core_box.go +++ b/cmd/mesh-controller/core_box.go @@ -13,10 +13,9 @@ import ( "sort" "strings" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "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, @@ -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 // 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 // opened. 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 // say is answered, not done. 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{}} modules := make([]string, 0, len(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] { k, has := kept[module][name] valid := has && k.SealedTo == sealingKey && len(k.Box) > 0 - if choosing != Allocating { + if !allocating { if valid { boxes[name] = k.Box } 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. 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) if len(core) == 0 { - if choosing == Allocating { + if allocating { settleVaultSilent(ctx, node) } return coreBoxes{}, nil @@ -235,16 +225,16 @@ func coreBoxesFor(ctx context.Context, inv *inventory.Inventory, node string, mo return coreBoxes{}, err } var ask coreAsk - if choosing == Allocating { + if allocating { ask = coreVault(ctx) } - out := coreBoxesOf(node, key, core, kept, choosing, ask) + out := coreBoxesOf(node, key, core, kept, allocating, ask) for _, b := range out.keep { if err := inv.KeepCoreBox(ctx, node, b); err != nil { return coreBoxes{}, err } } - if choosing == Allocating { + if allocating { sayVaultSilent(ctx, node, out) } return out, nil diff --git a/cmd/mesh-controller/core_box_integration_test.go b/cmd/mesh-controller/core_box_integration_test.go index 55660821..d26e0e56 100644 --- a/cmd/mesh-controller/core_box_integration_test.go +++ b/cmd/mesh-controller/core_box_integration_test.go @@ -10,8 +10,8 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) @@ -96,7 +96,7 @@ func TestASilentVaultHoldsTheKeptBoxAndSaysSo(t *testing.T) { func TestACoreModuleFirstPlacedWhileTheVaultIsSilentIsSealedByTheController(t *testing.T) { open, vault := aCoreMesh(t, link.VaultBoxContract) 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 { t.Fatal(err) } diff --git a/cmd/mesh-controller/core_box_test.go b/cmd/mesh-controller/core_box_test.go index 4a6872de..1148d0a1 100644 --- a/cmd/mesh-controller/core_box_test.go +++ b/cmd/mesh-controller/core_box_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" ) @@ -68,7 +68,7 @@ func TestReplay908(t *testing.T) { key := "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" core := map[string][]string{"nats": {"password"}} 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 { 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. 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 { 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") return coreAnswer{}, nil } - for _, choosing := range []Choosing{Reading, Foreseeing} { - if got := coreBoxesOf("anchor", key, core, kept, choosing, never); string(got.sealed["nats"]["broker"]) != `{"v":1}` { + for _, allocating := range []bool{false} { + if got := coreBoxesOf("anchor", key, core, kept, allocating, never); string(got.sealed["nats"]["broker"]) != `{"v":1}` { 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) { t.Errorf("with none kept, read %s", got.sealed["nats"]["broker"]) } } // 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") }) 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) diff --git a/cmd/mesh-controller/credential_on_assign_test.go b/cmd/mesh-controller/credential_on_assign_test.go index 9c8133a0..0cbc1262 100644 --- a/cmd/mesh-controller/credential_on_assign_test.go +++ b/cmd/mesh-controller/credential_on_assign_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) @@ -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. - plan, settings, err := planFor(ctx, open, "laptop") + plan, settings, err := resolveNode(ctx, open, "laptop") if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-controller/data.go b/cmd/mesh-controller/data.go index 142e2164..15a808c9 100644 --- a/cmd/mesh-controller/data.go +++ b/cmd/mesh-controller/data.go @@ -14,7 +14,7 @@ import ( "github.com/nats-io/nats.go" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" @@ -152,6 +152,8 @@ type consumerCopy struct { // probeData is D13. func probeData(ctx context.Context, d *doctor) ([]conditions.Observation, error) { + // One reading of the mesh for every machine this looks at. + ctx = withInputCache(ctx) if d.js == nil { return nil, errors.New("no bus to ask the machines over") } @@ -180,7 +182,7 @@ func probeData(ctx context.Context, d *doctor) ([]conditions.Observation, error) } var machines []*machine for _, n := range nodes { - plan, _, err := planFor(ctx, open, n.Name) + plan, _, err := resolveNode(ctx, open, n.Name) if err != nil { if ctx.Err() != nil { return nil, ctx.Err() @@ -849,7 +851,7 @@ func deleteRetiredData(ctx context.Context, conn *nats.Conn, open *stores, r inv return fmt.Errorf("%s of %s on %s was never measured, so where it is was never said; nothing was deleted", r.Item, r.Module, r.Machine) } - if plan, _, err := planFor(ctx, open, r.Machine); err == nil { + if plan, _, err := resolveNode(ctx, open, r.Machine); err == nil { for _, m := range plan.Modules { if _, still := m.DataItem(r.Item); still && m.Module == r.Module { return fmt.Errorf("%s runs on %s again and declares %s: it is not retired any more. Nothing was done", diff --git a/cmd/mesh-controller/data_integration_test.go b/cmd/mesh-controller/data_integration_test.go index 4d8a9853..68247082 100644 --- a/cmd/mesh-controller/data_integration_test.go +++ b/cmd/mesh-controller/data_integration_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/testbus" diff --git a/cmd/mesh-controller/data_test.go b/cmd/mesh-controller/data_test.go index 15dcf45e..a9589f77 100644 --- a/cmd/mesh-controller/data_test.go +++ b/cmd/mesh-controller/data_test.go @@ -11,7 +11,7 @@ import ( "github.com/novox/mesh-controller/internal/link" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/deadletters.go b/cmd/mesh-controller/deadletters.go index 74f22cb0..489256ee 100644 --- a/cmd/mesh-controller/deadletters.go +++ b/cmd/mesh-controller/deadletters.go @@ -9,8 +9,8 @@ import ( "github.com/nats-io/nats.go" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/delivery.go b/cmd/mesh-controller/delivery.go index c24b2f43..e8d35fd8 100644 --- a/cmd/mesh-controller/delivery.go +++ b/cmd/mesh-controller/delivery.go @@ -15,7 +15,7 @@ import ( "github.com/nats-io/nats.go" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) @@ -1179,7 +1179,7 @@ func askGroupCheck(ctx context.Context, open *stores, group string, members []or request.Check.Group = group // A group's check is queued as ready when any member's pull request is marked ready. request.Check.Ready = slices.ContainsFunc(members, func(m orderMember) bool { return m.Ready }) - world, err := theRestOfTheMesh(ctx, open.inventory, shelfOf(entries), "") + world, err := restOfTheMesh(ctx, open, shelfOf(entries), "") if err != nil { return "", err } diff --git a/cmd/mesh-controller/delivery_changes.go b/cmd/mesh-controller/delivery_changes.go index 2fb5f82f..0e2a2966 100644 --- a/cmd/mesh-controller/delivery_changes.go +++ b/cmd/mesh-controller/delivery_changes.go @@ -12,7 +12,7 @@ import ( "sync" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/delivery_conditions.go b/cmd/mesh-controller/delivery_conditions.go index d5fefbe8..74b14082 100644 --- a/cmd/mesh-controller/delivery_conditions.go +++ b/cmd/mesh-controller/delivery_conditions.go @@ -10,8 +10,8 @@ import ( "github.com/nats-io/nats.go" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/delivery_conditions_test.go b/cmd/mesh-controller/delivery_conditions_test.go index cb3176fa..c9533fac 100644 --- a/cmd/mesh-controller/delivery_conditions_test.go +++ b/cmd/mesh-controller/delivery_conditions_test.go @@ -13,8 +13,8 @@ import ( "github.com/nats-io/nats.go" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" diff --git a/cmd/mesh-controller/delivery_edges_test.go b/cmd/mesh-controller/delivery_edges_test.go index 195aa2eb..b858720c 100644 --- a/cmd/mesh-controller/delivery_edges_test.go +++ b/cmd/mesh-controller/delivery_edges_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/delivery_integration_test.go b/cmd/mesh-controller/delivery_integration_test.go index ed85ec2f..399df3a8 100644 --- a/cmd/mesh-controller/delivery_integration_test.go +++ b/cmd/mesh-controller/delivery_integration_test.go @@ -8,9 +8,9 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/nats-io/nats.go" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/testbus" @@ -122,7 +122,7 @@ func TestAMergeWaitsForItsDeliverysWordAndThePersonsWordWorksWithoutIt(t *testin advanceHeld(ctx, open) got, _ = inv.PlanByID(ctx, p.ID) if got.Waiting() || !strings.HasPrefix(got.Delivery.By, "a person") || len(*asked) < 4 { - t.Fatalf("a person's word did not start the walk: %+v, asked %v", got.Delivery, *asked) + t.Fatalf("a person's word did not start the walk: %+v, asked %v, state %q, tier %d, note %q", got.Delivery, *asked, got.State, got.Tier, got.Note) } // The owner's stop: failed, said as stopped by it. diff --git a/cmd/mesh-controller/delivery_test.go b/cmd/mesh-controller/delivery_test.go index d7f1b0df..a3127130 100644 --- a/cmd/mesh-controller/delivery_test.go +++ b/cmd/mesh-controller/delivery_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/dependents_test.go b/cmd/mesh-controller/dependents_test.go index 48cf8d9b..05c306b1 100644 --- a/cmd/mesh-controller/dependents_test.go +++ b/cmd/mesh-controller/dependents_test.go @@ -3,7 +3,7 @@ package main import ( "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/doctor_test.go b/cmd/mesh-controller/doctor_test.go index 8f5b0506..4c9e116e 100644 --- a/cmd/mesh-controller/doctor_test.go +++ b/cmd/mesh-controller/doctor_test.go @@ -10,8 +10,8 @@ import ( "golang.org/x/net/dns/dnsmessage" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/epoch_send_test.go b/cmd/mesh-controller/epoch_send_test.go index 9e138c3c..4fc2e5e4 100644 --- a/cmd/mesh-controller/epoch_send_test.go +++ b/cmd/mesh-controller/epoch_send_test.go @@ -42,12 +42,8 @@ func TestAMachineIsSentTheEpochOnlyOnceItSaysItReadsOne(t *testing.T) { if err := inv.RecordReadsEpoch(ctx, anchor.ID, true); err != nil { t.Fatal(err) } - gens, err := generators(ctx, open) - if err != nil { - t.Fatal(err) - } 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) } carried := func(node string) (epoch float64, has bool) { @@ -89,12 +85,8 @@ func TestNothingIsComposedOrSentWithoutTheLease(t *testing.T) { was := epochForActs epochForActs = func(context.Context) (uint64, error) { return 0, errors.New("this controller lost the lease") } 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{}} - 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 { t.Fatal(err) } diff --git a/cmd/mesh-controller/facts.go b/cmd/mesh-controller/facts.go index f1f38f76..67bd4c07 100644 --- a/cmd/mesh-controller/facts.go +++ b/cmd/mesh-controller/facts.go @@ -14,13 +14,12 @@ import ( "sync" "time" - "github.com/novox/mesh-host/validate" - + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + snapshot "git.novox.be/novox/mesh-sdk/go/facts" "github.com/novox/mesh-controller/internal/artifacts" "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/builder" - "github.com/novox/mesh-controller/internal/catalogue" - snapshot "github.com/novox/mesh-controller/internal/facts" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) @@ -170,10 +169,15 @@ func factsStore(ctx context.Context, open *stores) (string, error) { return address, nil } +// writtenFormat is the format of the snapshot this controller writes: 1, the format its own reader knew before mesh-sdk's +// go/facts took the type over (issue 1390, novox/hq ADR 0456). The readers (the merge gate, the check) read formats 1 and +// 2; the writer moves to 2, which also carries each machine's grants and bindings, in a later release of its own. +const writtenFormat = 1 + // gatherFacts composes one snapshot from the store: read only, nothing made, nothing sent. func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot.Facts, error) { inv := open.inventory - f := snapshot.Facts{Format: snapshot.Format, Taken: time.Now().UTC(), Controller: snapshot.Build{Version: version}} + f := snapshot.Facts{Format: writtenFormat, Taken: time.Now().UTC(), Controller: snapshot.Build{Version: version}} f.Versions.Bus = busVersion storeVersion, err := inv.ServerVersion(ctx) if err != nil { @@ -263,7 +267,9 @@ func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot f.Controller.Commit = c.Commit } - gens, gensErr := generators(ctx, open) + // The Input every machine's declaration below is composed over is read once for the whole snapshot. + ctx = withInputCache(ctx) + _, gensErr := generatorsOf(ctx, open) hostShelf := shelf[hostModule] meshWide := map[string]bool{} engines := map[string]bool{} @@ -361,7 +367,7 @@ func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot m.Settings = append(m.Settings, snapshot.Settings{Module: module, Values: scrub.Values(layer.Whole())}) } } - m.Declaration = declarationFacts(ctx, open, n.Name, gens, gensErr, scrub) + m.Declaration = declarationFacts(ctx, open, n.Name, gensErr, scrub) if ctx.Err() != nil { return snapshot.Facts{}, ctx.Err() } @@ -456,14 +462,14 @@ func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot // declarationFacts is how one machine's declaration composes now, as the next push would compose it and // without making anything (D1's composition), and whether the node-engine's validator takes it. -func declarationFacts(ctx context.Context, open *stores, node string, gens map[string]catalogue.Generator, - gensErr error, scrub *snapshot.Scrubber) snapshot.Declaration { +func declarationFacts(ctx context.Context, open *stores, node string, gensErr error, + scrub *snapshot.Scrubber) snapshot.Declaration { var d snapshot.Declaration if gensErr != nil { d.Problems = []string{scrub.Text("the private network cannot be computed: " + oneLine(gensErr.Error()))} return d } - declared, problems, err := composedAndValidated(ctx, open, node, gens, Foreseeing) + declared, problems, err := composeQuestion(ctx, open, node, compose.Foreseeing, true) if err != nil { d.Problems = []string{scrub.Text(oneLine(err.Error()))} return d @@ -496,41 +502,6 @@ func declarationFacts(ctx context.Context, open *stores, node string, gens map[s 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. func resourceNames(resources []map[string]any) []string { out := make([]string, 0, len(resources)) diff --git a/cmd/mesh-controller/facts_format_test.go b/cmd/mesh-controller/facts_format_test.go new file mode 100644 index 00000000..b5583fcd --- /dev/null +++ b/cmd/mesh-controller/facts_format_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "encoding/json" + "strings" + "testing" + "time" + + snapshot "git.novox.be/novox/mesh-sdk/go/facts" +) + +// 1390 (novox/hq ADR 0456): the snapshot this controller writes is format 1, and the reader it shares with the build seat +// (mesh-sdk's go/facts) takes formats 1 and 2 and refuses one newer than it knows. The writer moves to format 2 in a release +// of its own. It failed first with the controller's own reader, which refused format 2 as newer than it knew. +func TestTheWriterWritesFormatOneAndTheReaderTakesBothFormats(t *testing.T) { + if writtenFormat != 1 { + t.Fatalf("the writer writes format %d, not 1", writtenFormat) + } + taken := time.Date(2026, 10, 20, 9, 0, 0, 0, time.UTC) + for format, want := range map[int]bool{1: true, 2: true, 3: false} { + body, err := (snapshot.Facts{Format: format, Taken: taken}).Encode() + if err != nil { + t.Fatal(err) + } + f, err := snapshot.Decode(body) + switch { + case want && err != nil: + t.Errorf("format %d is refused: %v", format, err) + case want && f.Format != format: + t.Errorf("format %d is read as %d", format, f.Format) + case !want && (err == nil || !strings.Contains(err.Error(), "newer")): + t.Errorf("format %d is not refused as newer: %v", format, err) + } + } + // What the writer composes carries no field of format 2: a reader of 1 and of 2 read it alike. + body, err := (snapshot.Facts{Format: writtenFormat, Taken: taken, Machines: []snapshot.Machine{{Name: "abcdef", Length: 6}}}).Encode() + if err != nil { + t.Fatal(err) + } + var raw struct { + Format int `json:"facts"` + Machines []map[string]json.RawMessage `json:"machines"` + } + if err := json.Unmarshal(body, &raw); err != nil { + t.Fatal(err) + } + if raw.Format != 1 { + t.Errorf("the snapshot says format %d", raw.Format) + } + for _, m := range raw.Machines { + if _, has := m["grants"]; has { + t.Error("a format 1 snapshot carries grants") + } + if _, has := m["bindings"]; has { + t.Error("a format 1 snapshot carries bindings") + } + } +} diff --git a/cmd/mesh-controller/facts_test.go b/cmd/mesh-controller/facts_test.go index 7f3c9544..5edc0095 100644 --- a/cmd/mesh-controller/facts_test.go +++ b/cmd/mesh-controller/facts_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" - snapshot "github.com/novox/mesh-controller/internal/facts" + "git.novox.be/novox/mesh-sdk/go/catalogue" + snapshot "git.novox.be/novox/mesh-sdk/go/facts" ) // The facts snapshot (novox/hq to-be 45 §9): composed from the store, every machine under a pseudonym @@ -42,12 +42,8 @@ func aMeshWithSecrets(t *testing.T) (*stores, []string) { t.Fatal(err) } // 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"} { - 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) } } @@ -68,6 +64,9 @@ func TestTheFactsCarryNoSecretNoAddressAndNoName(t *testing.T) { if err != nil { t.Fatal(err) } + if f.Format != 1 { + t.Fatalf("the writer wrote format %d: it keeps format 1 until its own release moves it (issue 1390)", f.Format) + } body, err := f.Encode() if err != nil { t.Fatal(err) diff --git a/cmd/mesh-controller/first_build_ask_integration_test.go b/cmd/mesh-controller/first_build_ask_integration_test.go index 8f0a7ac8..68d84fdd 100644 --- a/cmd/mesh-controller/first_build_ask_integration_test.go +++ b/cmd/mesh-controller/first_build_ask_integration_test.go @@ -9,8 +9,8 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/asks" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/identity" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/first_builds_integration_test.go b/cmd/mesh-controller/first_builds_integration_test.go index 133c79a7..e82d7284 100644 --- a/cmd/mesh-controller/first_builds_integration_test.go +++ b/cmd/mesh-controller/first_builds_integration_test.go @@ -12,8 +12,8 @@ import ( "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/testbus" diff --git a/cmd/mesh-controller/fixclass_test.go b/cmd/mesh-controller/fixclass_test.go index 225089f9..9541c005 100644 --- a/cmd/mesh-controller/fixclass_test.go +++ b/cmd/mesh-controller/fixclass_test.go @@ -3,7 +3,7 @@ package main import ( "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/gate.go b/cmd/mesh-controller/gate.go index f0912525..30a7499d 100644 --- a/cmd/mesh-controller/gate.go +++ b/cmd/mesh-controller/gate.go @@ -13,8 +13,8 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/micro" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/lease" diff --git a/cmd/mesh-controller/gate_followup_test.go b/cmd/mesh-controller/gate_followup_test.go index 9ba07bf3..ca5dbb9f 100644 --- a/cmd/mesh-controller/gate_followup_test.go +++ b/cmd/mesh-controller/gate_followup_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/lease" diff --git a/cmd/mesh-controller/gate_integration_test.go b/cmd/mesh-controller/gate_integration_test.go index fff52fa6..31e5c56d 100644 --- a/cmd/mesh-controller/gate_integration_test.go +++ b/cmd/mesh-controller/gate_integration_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/lease" diff --git a/cmd/mesh-controller/gate_rule_test.go b/cmd/mesh-controller/gate_rule_test.go index 69681695..b1b1033e 100644 --- a/cmd/mesh-controller/gate_rule_test.go +++ b/cmd/mesh-controller/gate_rule_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/lease" ) diff --git a/cmd/mesh-controller/gate_test.go b/cmd/mesh-controller/gate_test.go index 589c4aa4..8109ede3 100644 --- a/cmd/mesh-controller/gate_test.go +++ b/cmd/mesh-controller/gate_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/lease" diff --git a/cmd/mesh-controller/generation.go b/cmd/mesh-controller/generation.go index b8d1203b..ce649ce0 100644 --- a/cmd/mesh-controller/generation.go +++ b/cmd/mesh-controller/generation.go @@ -6,11 +6,10 @@ import ( "fmt" "io" "os" - "sort" "strings" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" @@ -103,18 +102,6 @@ func senderOf(caller string) string { 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 // 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 diff --git a/cmd/mesh-controller/grant_follows_binding_integration_test.go b/cmd/mesh-controller/grant_follows_binding_integration_test.go index 74622b3d..0aed7741 100644 --- a/cmd/mesh-controller/grant_follows_binding_integration_test.go +++ b/cmd/mesh-controller/grant_follows_binding_integration_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" ) func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) { @@ -35,17 +35,21 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) { if err := inv.PinProvision(ctx, "laptop", "postgres-database", "anchor", "store"); err != nil { t.Fatal(err) } - plan, _, err := planFor(ctx, open, "laptop") + plan, _, err := resolveNode(ctx, open, "laptop") if err != nil { t.Fatal(err) } + // Only a send makes a credential; a question makes nothing. + if _, err := composeSend(ctx, open, "laptop"); err != nil { + t.Fatal(err) + } if n := need(t, plan, "board", "postgres-database"); n.From != "anchor" { t.Fatalf("pinned to the anchor and bound to %s", n.From) } if err := inv.RecordBindings(ctx, "laptop", boundToData(plan, nil)); err != nil { t.Fatal(err) } - grants, _, unbound, err := grantsFor(ctx, open, "anchor") + grants, _, unbound, err := grantsOf(ctx, open, "anchor") if err != nil { t.Fatal(err) } @@ -57,10 +61,13 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) { if err := inv.PinProvision(ctx, "laptop", "postgres-database", "laptop", "store"); err != nil { t.Fatal(err) } - plan, _, err = planFor(ctx, open, "laptop") + plan, _, err = resolveNode(ctx, open, "laptop") if err != nil { t.Fatal(err) } + if _, err := composeSend(ctx, open, "laptop"); err != nil { + t.Fatal(err) + } if n := need(t, plan, "board", "postgres-database"); n.From != "laptop" { t.Fatalf("pinned back to the laptop and bound to %s", n.From) } @@ -76,7 +83,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) { } // 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 { t.Fatal(err) } @@ -87,7 +94,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) { strings.Join(unbound[0].BoundTo, ",") != "laptop" { 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 { t.Fatal(err) } @@ -105,7 +112,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) { } // The store it is bound to: granted. - grants, _, unbound, err = grantsFor(ctx, open, "laptop") + grants, _, unbound, err = grantsOf(ctx, open, "laptop") if err != nil { t.Fatal(err) } @@ -131,7 +138,7 @@ func TestAConsumerWhoseResolutionCannotBeReadIsNotWithdrawn(t *testing.T) { t.Fatalf("assign %s %s: %v", a[0], a[1], err) } } - if _, _, err := planFor(ctx, open, "laptop"); err != nil { + if _, err := composeSend(ctx, open, "laptop"); err != nil { t.Fatal(err) } @@ -144,14 +151,15 @@ func TestAConsumerWhoseResolutionCannotBeReadIsNotWithdrawn(t *testing.T) { if err := inv.RecordSealingKey(ctx, laptop.ID, "not a key"); err != nil { 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) } - grants, _, unbound, err := grantsFor(ctx, open, "anchor") + grants, _, unbound, err := grantsOf(ctx, open, "anchor") if err == nil { t.Fatalf("an unreadable consumer was answered: grants %+v, unbound %+v", grants, unbound) } - if !strings.Contains(err.Error(), "what laptop asked of postgres-database cannot be read") { + // mesh-sdk names the provider's node where the controller's own words named the provision. + if !strings.Contains(err.Error(), "what laptop asked of") || !strings.Contains(err.Error(), "cannot be read") { t.Fatalf("the error does not say whose resolution could not be read: %v", err) } } diff --git a/cmd/mesh-controller/grant_follows_binding_test.go b/cmd/mesh-controller/grant_follows_binding_test.go index 9b9c6cd4..2918e1f0 100644 --- a/cmd/mesh-controller/grant_follows_binding_test.go +++ b/cmd/mesh-controller/grant_follows_binding_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/grants_step_integration_test.go b/cmd/mesh-controller/grants_step_integration_test.go index c40c1bd3..7e54c6d8 100644 --- a/cmd/mesh-controller/grants_step_integration_test.go +++ b/cmd/mesh-controller/grants_step_integration_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" ) // The store test's manifests are manifests the controller takes in: checked here, where no store is needed, diff --git a/cmd/mesh-controller/grants_step_store_test.go b/cmd/mesh-controller/grants_step_store_test.go index 705dc8fb..8432483d 100644 --- a/cmd/mesh-controller/grants_step_store_test.go +++ b/cmd/mesh-controller/grants_step_store_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/identity" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" @@ -66,15 +66,11 @@ func TestTheGrantsStepSendsTheUserListAndNothingElse(t *testing.T) { if err != nil { t.Fatal(err) } - plan, settings, err := planFor(under, open, "anchor") + plan, _, err := resolveNode(under, open, "anchor") if err != nil { t.Fatal(err) } - gens, err := generators(under, open) - if err != nil { - t.Fatal(err) - } - declared, err := declarationWith(under, open, "anchor", plan, settings, gens, Allocating) + declared, err := composeSend(under, open, "anchor") if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-controller/grants_step_test.go b/cmd/mesh-controller/grants_step_test.go index 48b34bb5..c484d693 100644 --- a/cmd/mesh-controller/grants_step_test.go +++ b/cmd/mesh-controller/grants_step_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/group_wait.go b/cmd/mesh-controller/group_wait.go index 46a22e03..8713318e 100644 --- a/cmd/mesh-controller/group_wait.go +++ b/cmd/mesh-controller/group_wait.go @@ -11,7 +11,7 @@ import ( "github.com/nats-io/nats.go" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/health_check_test.go b/cmd/mesh-controller/health_check_test.go index 5ac0e1cc..4c4ddf53 100644 --- a/cmd/mesh-controller/health_check_test.go +++ b/cmd/mesh-controller/health_check_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" ) // Every catalogue module that runs something long-lived declares how it is ready (novox/hq ADR 0240 rule diff --git a/cmd/mesh-controller/health_probes.go b/cmd/mesh-controller/health_probes.go index c0e59764..d9cc994c 100644 --- a/cmd/mesh-controller/health_probes.go +++ b/cmd/mesh-controller/health_probes.go @@ -10,8 +10,8 @@ import ( "github.com/nats-io/nats.go" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/lease" diff --git a/cmd/mesh-controller/held_back.go b/cmd/mesh-controller/held_back.go index 66644ec2..16ffc4d5 100644 --- a/cmd/mesh-controller/held_back.go +++ b/cmd/mesh-controller/held_back.go @@ -7,7 +7,6 @@ import ( "sort" "strings" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) @@ -107,6 +106,8 @@ func buildName(commit string) string { // (heldBack), and answers only the machines held. A machine whose set cannot be worked out is left // to the send, which says why. func heldMachines(ctx context.Context, open *stores, names []string) (map[string][]string, error) { + // One reading of the mesh for every machine this looks at. + ctx = withInputCache(ctx) out := map[string][]string{} if len(names) == 0 { return out, nil @@ -125,7 +126,7 @@ func heldMachines(ctx context.Context, open *stores, names []string) (map[string return nil, err } for _, node := range names { - plan, _, err := planFor(ctx, open, node) + plan, _, err := resolveNode(ctx, open, node) if err != nil { continue } @@ -263,15 +264,17 @@ 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 // 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) { - plan, settings, err := planFor(held, open, node) + // Read afresh for each machine: the send before it made and recorded things. + held = freshInput(held) + plan, _, err := resolveNode(held, open, node) if err != nil { return sendable{}, err } reportUnhostable(node, plan) reportKept(held, plan) - declared, err := declarationWith(held, open, node, plan, settings, gens, Allocating) + declared, err := composeSend(held, open, node) if err == nil { reportLeftOut(node, declared) } diff --git a/cmd/mesh-controller/held_back_integration_test.go b/cmd/mesh-controller/held_back_integration_test.go index e8e2eb5d..dc0b3d7d 100644 --- a/cmd/mesh-controller/held_back_integration_test.go +++ b/cmd/mesh-controller/held_back_integration_test.go @@ -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 { t.Fatal(err) } - gens, err := generators(ctx, open) - if err != nil { - t.Fatal(err) - } - compose := composeForPush(open, gens) + compose := composeForPush(open) d := &recordedDelivery{inv: inv} if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil { t.Fatal(err) @@ -134,11 +130,7 @@ func TestANamedPushStillSendsAConsequenceNothingHolds(t *testing.T) { if _, err := inv.Assign(ctx, "laptop", "resolver"); err != nil { t.Fatal(err) } - gens, err := generators(ctx, open) - if err != nil { - t.Fatal(err) - } - compose := composeForPush(open, gens) + compose := composeForPush(open) d := &recordedDelivery{inv: inv} if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil { t.Fatal(err) diff --git a/cmd/mesh-controller/held_back_test.go b/cmd/mesh-controller/held_back_test.go index 2d5eb506..05e7c65a 100644 --- a/cmd/mesh-controller/held_back_test.go +++ b/cmd/mesh-controller/held_back_test.go @@ -3,14 +3,13 @@ package main import ( "context" "encoding/json" - "reflect" "strings" "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/overlay" ) // novox/hq issue 259, ADR 0221: a push that did not name a machine does not send it a build its @@ -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 // reads the machine as current — and writes down which machines it declared. type recordedDelivery struct { diff --git a/cmd/mesh-controller/holders_silent_test.go b/cmd/mesh-controller/holders_silent_test.go index 79362ec9..0f1486f6 100644 --- a/cmd/mesh-controller/holders_silent_test.go +++ b/cmd/mesh-controller/holders_silent_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" ) // The live case of 2026-10-07 (novox/hq issue 299), replayed: the node-uplink seat gained its first diff --git a/cmd/mesh-controller/holdings.go b/cmd/mesh-controller/holdings.go index 3eab711c..d4893725 100644 --- a/cmd/mesh-controller/holdings.go +++ b/cmd/mesh-controller/holdings.go @@ -5,9 +5,9 @@ import ( "fmt" "sort" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/overlay" ) // recordDerivedHolders writes down who holds each mesh-scoped seat that nobody was ever recorded @@ -36,7 +36,7 @@ func recordDerivedHolders(ctx context.Context, open *stores) ([]string, error) { return nil, err } // exclude nobody: every node's claims, resolved with the holdings on record. - world, err := theRestOfTheMesh(ctx, inv, shelf, "") + world, err := restOfTheMesh(ctx, open, shelf, "") if err != nil { return nil, err } diff --git a/cmd/mesh-controller/holdings_test.go b/cmd/mesh-controller/holdings_test.go index efab46cd..420360c4 100644 --- a/cmd/mesh-controller/holdings_test.go +++ b/cmd/mesh-controller/holdings_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" ) // A seat nobody ever handed over is held by whichever assignment happened to be alone — and the @@ -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. 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 { t.Fatalf("%s no longer resolves: %v", node, err) } diff --git a/cmd/mesh-controller/identity_bound_integration_test.go b/cmd/mesh-controller/identity_bound_integration_test.go index 8195ab04..01774c33 100644 --- a/cmd/mesh-controller/identity_bound_integration_test.go +++ b/cmd/mesh-controller/identity_bound_integration_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" ) func TestAnOverflowingConsumerNeverRefusesItsProvidersMachine(t *testing.T) { @@ -31,7 +31,7 @@ func TestAnOverflowingConsumerNeverRefusesItsProvidersMachine(t *testing.T) { } // 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 { 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 // 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 { t.Fatal(err) } @@ -53,7 +53,7 @@ func TestAnOverflowingConsumerNeverRefusesItsProvidersMachine(t *testing.T) { 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) } - grants, _, _, err := grantsFor(ctx, open, "anchor") + grants, _, _, err := grantsOf(ctx, open, "anchor") if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-controller/issue201_integration_test.go b/cmd/mesh-controller/issue201_integration_test.go index 413b2f1c..a138642c 100644 --- a/cmd/mesh-controller/issue201_integration_test.go +++ b/cmd/mesh-controller/issue201_integration_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/lease" ) @@ -79,7 +79,7 @@ func TestReplay201(t *testing.T) { 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"}} - 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) } key := coreDowngradeObservation(lease.ComponentController, "anchor", "").Key() diff --git a/cmd/mesh-controller/issue348_integration_test.go b/cmd/mesh-controller/issue348_integration_test.go index b2c5f29c..a723bda0 100644 --- a/cmd/mesh-controller/issue348_integration_test.go +++ b/cmd/mesh-controller/issue348_integration_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/lease" diff --git a/cmd/mesh-controller/issue352_integration_test.go b/cmd/mesh-controller/issue352_integration_test.go index 7f604afe..1f7c399e 100644 --- a/cmd/mesh-controller/issue352_integration_test.go +++ b/cmd/mesh-controller/issue352_integration_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/issue352_test.go b/cmd/mesh-controller/issue352_test.go index 3baa9b57..86e8cea5 100644 --- a/cmd/mesh-controller/issue352_test.go +++ b/cmd/mesh-controller/issue352_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/lease" ) diff --git a/cmd/mesh-controller/issue648_test.go b/cmd/mesh-controller/issue648_test.go index 89fc9bb5..af1ba7d5 100644 --- a/cmd/mesh-controller/issue648_test.go +++ b/cmd/mesh-controller/issue648_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/issued.go b/cmd/mesh-controller/issued.go index 4ae02291..4c553807 100644 --- a/cmd/mesh-controller/issued.go +++ b/cmd/mesh-controller/issued.go @@ -10,8 +10,8 @@ import ( "github.com/nats-io/nats.go" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/secrets" diff --git a/cmd/mesh-controller/issued_test.go b/cmd/mesh-controller/issued_test.go index f2819a76..baa1b90a 100644 --- a/cmd/mesh-controller/issued_test.go +++ b/cmd/mesh-controller/issued_test.go @@ -5,7 +5,9 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/secrets" @@ -138,42 +140,73 @@ func TestAnIssuedValueIsADecisionInTheHandActLog(t *testing.T) { t.Fatal("secret issued is not in the hand-act table") } +// needOf is the need a consumer has of a provision, as mesh-sdk's resolution gives it (the controller's issuedToHolder and +// providerMoved became the fields Needed.IssuedToHolder and ProviderMoved of compose.Resolve, issue 1389): the shelf, the +// provider on one machine and the consumer on another. +func needOf(t *testing.T, shelf map[string]catalogue.Manifest, provider, consumer, provision string) catalogue.Needed { + t.Helper() + all := map[string]catalogue.Manifest{} + for name, m := range shelf { + all[name] = m + } + all[consumer] = catalogue.Manifest{Module: consumer, Version: "1", Requires: []string{provision}} + // The private network comes with the control plane, and both machines run it. + var network []string + for _, m := range provided { + all[m.Module] = m + if m.Module == overlay.Name { + network = append(network, m.Module) + } + } + in := compose.Input{Catalogue: all, OverlayCIDR: "10.77.0.0/16", InternalSuffix: "internal", + Nodes: []compose.Node{ + {Name: "novox", Assigned: append([]string{provider}, network...), Place: compose.Place{Address: "10.77.0.1"}}, + {Name: "ace", Assigned: append([]string{consumer}, network...), Place: compose.Place{Address: "10.77.0.2"}}}} + resolved, err := compose.Resolve(in, "ace") + if err != nil { + t.Fatalf("%s does not resolve beside %s: %v", consumer, provider, err) + } + for _, n := range resolved.Plan.Needs { + if n.For == consumer && n.Name == provision { + return n + } + } + t.Fatalf("%s has no need of %s: %+v", consumer, provision, resolved.Plan.Needs) + return catalogue.Needed{} +} + // **A need's credential is the vault's for its holder alone** (issue 757) exactly when its providing module is marked // moved, so `secret issued` goes to the vault naming each holder, and its software issues the provision's credential. func TestANeedIsIssuedToItsHolderOnlyWhereTheProviderIsMovedAndItsSoftwareIssuesIt(t *testing.T) { forge := func(moved bool, issued bool) catalogue.Manifest { - offer := catalogue.Offer{Name: "gitea-read"} + offer := catalogue.Offer{Name: "gitea-read", Scope: catalogue.ScopeMesh} if issued { offer.Credential = &catalogue.OfferCredential{IssuedBy: catalogue.IssuedBySoftware, Tool: "gitea_agent_token"} } - return catalogue.Manifest{Module: "gitea", Moved: moved, Provides: []catalogue.Offer{offer}} + return catalogue.Manifest{Module: "gitea", Version: "1", Moved: moved, Provides: []catalogue.Offer{offer}} } shared := func(moved bool) catalogue.Manifest { - return catalogue.Manifest{Module: "gitea", Moved: moved, OwnSecrets: catalogue.OwnSecrets{"key": {Path: "/k"}}, - Provides: []catalogue.Offer{{Name: "gitea-read", Credential: &catalogue.OfferCredential{Own: "key"}}}} + return catalogue.Manifest{Module: "gitea", Version: "1", Moved: moved, OwnSecrets: catalogue.OwnSecrets{"key": {Path: "/k"}}, + Provides: []catalogue.Offer{{Name: "gitea-read", Scope: catalogue.ScopeMesh, Credential: &catalogue.OfferCredential{Own: "key"}}}} } given := func() catalogue.Manifest { - return catalogue.Manifest{Module: "gitea", Moved: true, Provides: []catalogue.Offer{{Name: "gitea-read", - Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}} + return catalogue.Manifest{Module: "gitea", Version: "1", Moved: true, Provides: []catalogue.Offer{{Name: "gitea-read", + Scope: catalogue.ScopeMesh, Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}} } - need := catalogue.Needed{Name: "gitea-read", From: "novox", Module: "gitea", For: "claude-code"} for why, c := range map[string]struct { - shelf map[string]catalogue.Manifest - need catalogue.Needed + forge catalogue.Manifest want bool }{ - "moved and issued by its software": {map[string]catalogue.Manifest{"gitea": forge(true, true)}, need, true}, - "not moved": {map[string]catalogue.Manifest{"gitea": forge(false, true)}, need, false}, - "a login for each consumer": {map[string]catalogue.Manifest{"gitea": forge(true, false)}, need, true}, - "a login for each, not moved": {map[string]catalogue.Manifest{"gitea": forge(false, false)}, need, false}, - "a credential every holder shares": {map[string]catalogue.Manifest{"gitea": shared(true)}, need, false}, - "a credential a person gives": {map[string]catalogue.Manifest{"gitea": given()}, need, false}, - "a provider not on the shelf": {map[string]catalogue.Manifest{}, need, false}, - "answered by a record": {map[string]catalogue.Manifest{"gitea": forge(true, true)}, - catalogue.Needed{Name: "gitea-read", ByRecord: true, Module: "gitea", For: "claude-code"}, false}, + "moved and issued by its software": {forge(true, true), true}, + "not moved": {forge(false, true), false}, + "a login for each consumer": {forge(true, false), true}, + "a login for each, not moved": {forge(false, false), false}, + "a credential every holder shares": {shared(true), false}, + "a credential a person gives": {given(), false}, } { - if got := issuedToHolder(c.shelf, c.need); got != c.want { - t.Errorf("%s: issued to its holder is %v, want %v", why, got, c.want) + got := needOf(t, map[string]catalogue.Manifest{"gitea": c.forge}, "gitea", "claude-code", "gitea-read") + if got.IssuedToHolder != c.want { + t.Errorf("%s: issued to its holder is %v, want %v", why, got.IssuedToHolder, c.want) } } } @@ -182,17 +215,14 @@ func TestANeedIsIssuedToItsHolderOnlyWhereTheProviderIsMovedAndItsSoftwareIssues // V3, so a need of `secret` is issued to its holder and answered by a moved provider, whether or not the module is // marked moved; another module is not. func TestTheModuleHoldingTheVaultSeatCountsAsMovedForItsConsumers(t *testing.T) { - vault := catalogue.Manifest{Module: "mesh-vault", Claims: []catalogue.Claim{{Name: "mesh-vault"}}, - Provides: []catalogue.Offer{{Name: "secret"}}} - other := catalogue.Manifest{Module: "mesh-vault", Provides: []catalogue.Offer{{Name: "secret"}}} - need := catalogue.Needed{Name: "secret", Local: "admin", From: "novox", Module: "mesh-vault", For: "gitea"} - if !issuedToHolder(map[string]catalogue.Manifest{"mesh-vault": vault}, need) || - !providerMoved(map[string]catalogue.Manifest{"mesh-vault": vault}, need) { - t.Error("the vault's consumer is not answered by a moved provider") + vault := catalogue.Manifest{Module: "mesh-vault", Version: "1", Claims: []catalogue.Claim{{Name: "mesh-vault", Scope: catalogue.ScopeMesh}}, + Provides: []catalogue.Offer{{Name: "secret", Scope: catalogue.ScopeMesh}}} + other := catalogue.Manifest{Module: "mesh-vault", Version: "1", Provides: []catalogue.Offer{{Name: "secret", Scope: catalogue.ScopeMesh}}} + if got := needOf(t, map[string]catalogue.Manifest{"mesh-vault": vault}, "mesh-vault", "gitea", "secret"); !got.IssuedToHolder || !got.ProviderMoved { + t.Errorf("the vault's consumer is not answered by a moved provider: %+v", got) } - if issuedToHolder(map[string]catalogue.Manifest{"mesh-vault": other}, need) || - providerMoved(map[string]catalogue.Manifest{"mesh-vault": other}, need) { - t.Error("a module not holding the vault seat counts as moved") + if got := needOf(t, map[string]catalogue.Manifest{"mesh-vault": other}, "mesh-vault", "gitea", "secret"); got.IssuedToHolder || got.ProviderMoved { + t.Errorf("a module not holding the vault seat counts as moved: %+v", got) } } @@ -200,21 +230,21 @@ func TestTheModuleHoldingTheVaultSeatCountsAsMovedForItsConsumers(t *testing.T) // provides entry says `"answered-by": "helper"`, the module need not be marked moved, and no credential is the vault's // for it. Another provision of the module, or a module without the entry, does not count. func TestAnOfferAnsweredByTheHelperCountsAsMovedForThatProvisionOnly(t *testing.T) { - proxy := catalogue.Manifest{Module: "route-proxy", Provides: []catalogue.Offer{ - {Name: "route", AnsweredBy: catalogue.AnsweredByHelper}, {Name: "other"}}} - plain := catalogue.Manifest{Module: "route-proxy", Provides: []catalogue.Offer{{Name: "route"}}} + proxy := catalogue.Manifest{Module: "route-proxy", Version: "1", Provides: []catalogue.Offer{ + {Name: "route", Scope: catalogue.ScopeMesh, AnsweredBy: catalogue.AnsweredByHelper}, {Name: "other", Scope: catalogue.ScopeMesh}}} + plain := catalogue.Manifest{Module: "route-proxy", Version: "1", Provides: []catalogue.Offer{{Name: "route", Scope: catalogue.ScopeMesh}}} shelf := func(m catalogue.Manifest) map[string]catalogue.Manifest { return map[string]catalogue.Manifest{m.Module: m} } - route := catalogue.Needed{Name: "route", From: "novox", Module: "route-proxy", For: "gitea"} - other := catalogue.Needed{Name: "other", From: "novox", Module: "route-proxy", For: "gitea"} - if !providerMoved(shelf(proxy), route) { + if got := needOf(t, shelf(proxy), "route-proxy", "gitea", "route"); !got.ProviderMoved { t.Error("a provision answered by the helper is not answered by a moved provider") - } - if providerMoved(shelf(proxy), other) || providerMoved(shelf(plain), route) { - t.Error("a provision not answered by the helper counts as moved") - } - if issuedToHolder(shelf(proxy), route) { + } else if got.IssuedToHolder { t.Error("the flag made a credential the vault's") } + if got := needOf(t, shelf(proxy), "route-proxy", "gitea", "other"); got.ProviderMoved { + t.Error("a provision not answered by the helper counts as moved") + } + if got := needOf(t, shelf(plain), "route-proxy", "gitea", "route"); got.ProviderMoved { + t.Error("a module without the entry counts as moved") + } } diff --git a/cmd/mesh-controller/issued_to_vault.go b/cmd/mesh-controller/issued_to_vault.go index 212a3d16..41703413 100644 --- a/cmd/mesh-controller/issued_to_vault.go +++ b/cmd/mesh-controller/issued_to_vault.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/issued_to_vault_test.go b/cmd/mesh-controller/issued_to_vault_test.go index 6f518918..50068d32 100644 --- a/cmd/mesh-controller/issued_to_vault_test.go +++ b/cmd/mesh-controller/issued_to_vault_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" ) // Every other test mints without a vault to hand to: a stand-in that takes anything, replaced by the tests below. diff --git a/cmd/mesh-controller/left_out.go b/cmd/mesh-controller/left_out.go index c66a21d3..94d1b9e5 100644 --- a/cmd/mesh-controller/left_out.go +++ b/cmd/mesh-controller/left_out.go @@ -6,7 +6,7 @@ import ( "sort" "strings" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" ) diff --git a/cmd/mesh-controller/left_out_integration_test.go b/cmd/mesh-controller/left_out_integration_test.go index feae280f..bcb7f947 100644 --- a/cmd/mesh-controller/left_out_integration_test.go +++ b/cmd/mesh-controller/left_out_integration_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" diff --git a/cmd/mesh-controller/left_out_test.go b/cmd/mesh-controller/left_out_test.go index 795bef55..dbe9c7c1 100644 --- a/cmd/mesh-controller/left_out_test.go +++ b/cmd/mesh-controller/left_out_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" ) diff --git a/cmd/mesh-controller/local_network.go b/cmd/mesh-controller/local_network.go index f3af5d91..8cd4ec06 100644 --- a/cmd/mesh-controller/local_network.go +++ b/cmd/mesh-controller/local_network.go @@ -1,30 +1,12 @@ package main import ( - "context" "fmt" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/overlay" ) -// localNetworksOf is a machine's attached networks judged (novox/hq ADR 0340): which count as its local -// network, from what it last said of itself and the links it said face outside it. A machine that has -// said none judges to nothing, and a port reached from `local-network` is then admitted from the private -// network only, and said so. -func localNetworksOf(ctx context.Context, inv *inventory.Inventory, node string) ([]catalogue.LocalNetwork, error) { - account, had, err := inv.MachineOf(ctx, node) - if err != nil { - return nil, err - } - outward, err := inv.OutwardLinksOf(ctx, node) - if err != nil { - return nil, err - } - return judgedNetworks(account, had, outward, overlay.Interface), nil -} - // judgedNetworks is the judging alone, from what was read. func judgedNetworks(account inventory.MachineAccount, had bool, outward []string, tunnel string) []catalogue.LocalNetwork { if !had || account.Machine == nil { diff --git a/cmd/mesh-controller/local_network_test.go b/cmd/mesh-controller/local_network_test.go index 739f39d2..0c46895e 100644 --- a/cmd/mesh-controller/local_network_test.go +++ b/cmd/mesh-controller/local_network_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/lone_merge_test.go b/cmd/mesh-controller/lone_merge_test.go index 4c9eb8f6..e5850d76 100644 --- a/cmd/mesh-controller/lone_merge_test.go +++ b/cmd/mesh-controller/lone_merge_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/merge_gate.go b/cmd/mesh-controller/merge_gate.go index 5557b96e..dad7db22 100644 --- a/cmd/mesh-controller/merge_gate.go +++ b/cmd/mesh-controller/merge_gate.go @@ -21,10 +21,10 @@ import ( "github.com/jackc/pgx/v5" + "git.novox.be/novox/mesh-sdk/go/catalogue" + snapshot "git.novox.be/novox/mesh-sdk/go/facts" "github.com/novox/mesh-controller/internal/artifacts" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" - snapshot "github.com/novox/mesh-controller/internal/facts" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/store" @@ -972,7 +972,7 @@ func composeEveryMachine(ctx context.Context, in mergeCheckInput, shelf map[stri if err != nil { return nil, nil, notes, err } - gens, gensErr := generators(ctx, open) + gens, gensErr := generatorsOf(ctx, open) out := map[string]mergeComposed{} for pass := 0; pass < 2; pass++ { for _, m := range in.facts.Machines { @@ -981,7 +981,7 @@ func composeEveryMachine(ctx context.Context, in mergeCheckInput, shelf map[stri oneLine(gensErr.Error())}} continue } - declared, problems, err := composedAndValidated(ctx, open, m.Name, gens, Allocating) + declared, problems, err := composeSendValidated(ctx, open, m.Name) if pass == 0 { continue } @@ -1047,7 +1047,7 @@ func tryAssigning(ctx context.Context, open *stores, f snapshot.Facts, m catalog if _, err := open.inventory.Assign(ctx, machine.Name, m.Module); err != nil { return mergeTrial{Machine: machine.Described(), Said: oneLine(err.Error())} } - _, problems, err := composedAndValidated(ctx, open, machine.Name, gens, Allocating) + _, problems, err := composeSendValidated(ctx, open, machine.Name) _ = open.inventory.Unassign(ctx, machine.Name, m.Module) t := mergeTrial{Machine: machine.Described()} if err != nil { diff --git a/cmd/mesh-controller/merge_gate_handing_integration_test.go b/cmd/mesh-controller/merge_gate_handing_integration_test.go index 74720f4c..58259195 100644 --- a/cmd/mesh-controller/merge_gate_handing_integration_test.go +++ b/cmd/mesh-controller/merge_gate_handing_integration_test.go @@ -7,7 +7,8 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) @@ -55,8 +56,12 @@ func TestIssue969TheGateRaisesEachMachineAsItsEngineStatedItself(t *testing.T) { t.Fatal(err) } for name, hands := range want { - got, err := engineTakesHanded(ctx, raised.inventory, name) - if err != nil || got != hands { + in, err := readComposeInput(ctx, raised) + if err != nil { + t.Fatal(err) + } + node, _ := nodeOf(in.Input, name) + if got := node.Stated && node.Contract >= compose.HandedContract; got != hands { t.Errorf("%s: the gate raises it as taking handed files = %v (%v), the mesh says %v", name, got, err, hands) } } diff --git a/cmd/mesh-controller/merge_gate_handing_test.go b/cmd/mesh-controller/merge_gate_handing_test.go index 4dfa0c91..73c5a9da 100644 --- a/cmd/mesh-controller/merge_gate_handing_test.go +++ b/cmd/mesh-controller/merge_gate_handing_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - snapshot "github.com/novox/mesh-controller/internal/facts" + snapshot "git.novox.be/novox/mesh-sdk/go/facts" "github.com/novox/mesh-controller/internal/link" ) @@ -14,15 +14,15 @@ func TestIssue969TheGateGivesAMachineItsContractSoItTakesHandedFiles(t *testing. taken := time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC) stated, ok := standInHealth(snapshot.Machine{Name: "f35", Contract: link.HandedContract}, taken) - if !ok || !takesHanded(stated, ok) { + if !ok || stated.Contract < link.HandedContract { t.Fatalf("a machine whose engine takes handed files must be given that in the gate: %+v, %v", stated, ok) } old, ok := standInHealth(snapshot.Machine{Name: "e12", Contract: link.HandedContract - 1}, taken) - if !ok || takesHanded(old, ok) { + if !ok || old.Contract >= link.HandedContract { t.Fatalf("a machine whose engine is older must not take handed files: %+v, %v", old, ok) } none, ok := standInHealth(snapshot.Machine{Name: "d01"}, taken) - if ok || takesHanded(none, ok) { + if ok { t.Fatalf("a snapshot that carries no contract states nothing: %+v, %v", none, ok) } } diff --git a/cmd/mesh-controller/merge_gate_integration_test.go b/cmd/mesh-controller/merge_gate_integration_test.go index 57ade01d..32965523 100644 --- a/cmd/mesh-controller/merge_gate_integration_test.go +++ b/cmd/mesh-controller/merge_gate_integration_test.go @@ -8,9 +8,9 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-sdk/go/catalogue" + snapshot "git.novox.be/novox/mesh-sdk/go/facts" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" - snapshot "github.com/novox/mesh-controller/internal/facts" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/mesh_for_test.go b/cmd/mesh-controller/mesh_for_test.go index aad02830..68be9da6 100644 --- a/cmd/mesh-controller/mesh_for_test.go +++ b/cmd/mesh-controller/mesh_for_test.go @@ -12,10 +12,10 @@ import ( "github.com/novox/mesh-controller/internal/conditions" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/licences" - "github.com/novox/mesh-controller/internal/overlay" ) // A mesh a command can be run against. diff --git a/cmd/mesh-controller/mirrors.go b/cmd/mesh-controller/mirrors.go index 901d3a8c..a3e8f2a5 100644 --- a/cmd/mesh-controller/mirrors.go +++ b/cmd/mesh-controller/mirrors.go @@ -10,8 +10,8 @@ import ( "regexp" "strings" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/artifacts" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/mirrors_integration_test.go b/cmd/mesh-controller/mirrors_integration_test.go index b2e5cfc7..c45687f4 100644 --- a/cmd/mesh-controller/mirrors_integration_test.go +++ b/cmd/mesh-controller/mirrors_integration_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/mirrors_test.go b/cmd/mesh-controller/mirrors_test.go index fee175df..af0f21aa 100644 --- a/cmd/mesh-controller/mirrors_test.go +++ b/cmd/mesh-controller/mirrors_test.go @@ -9,8 +9,8 @@ import ( "sync" "testing" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/artifacts" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/module_health.go b/cmd/mesh-controller/module_health.go index 39ba6f01..f021b1b8 100644 --- a/cmd/mesh-controller/module_health.go +++ b/cmd/mesh-controller/module_health.go @@ -8,7 +8,7 @@ import ( "sync/atomic" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" diff --git a/cmd/mesh-controller/module_health_test.go b/cmd/mesh-controller/module_health_test.go index f3706247..c28989d4 100644 --- a/cmd/mesh-controller/module_health_test.go +++ b/cmd/mesh-controller/module_health_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-sdk/go/compose" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" @@ -172,11 +173,12 @@ func TestHealthIsSentOnlyToAnEngineThatSaysItReadsIt(t *testing.T) { inv := open.inventory reads := func() bool { t.Helper() - got, err := engineReadsHealth(ctx, inv, "anchor") + in, err := readComposeInput(ctx, open) if err != nil { t.Fatal(err) } - return got + node, _ := nodeOf(in.Input, "anchor") + return node.Stated && node.Contract >= compose.ReadinessContract } if reads() { t.Fatal("an engine that never stated anything is sent health") diff --git a/cmd/mesh-controller/modules.go b/cmd/mesh-controller/modules.go index 030f2af0..12b84e9d 100644 --- a/cmd/mesh-controller/modules.go +++ b/cmd/mesh-controller/modules.go @@ -13,11 +13,11 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" - "github.com/novox/mesh-controller/internal/overlay" ) // the catalogue: what exists, what is assigned, and how it is configured. diff --git a/cmd/mesh-controller/modules_test.go b/cmd/mesh-controller/modules_test.go index 4de406cf..bbec2147 100644 --- a/cmd/mesh-controller/modules_test.go +++ b/cmd/mesh-controller/modules_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" ) // `module issue` makes a broker account and delivers it as the module's own secret named broker. diff --git a/cmd/mesh-controller/modules_wire_store_test.go b/cmd/mesh-controller/modules_wire_store_test.go index a708ddcd..ea3fcd6f 100644 --- a/cmd/mesh-controller/modules_wire_store_test.go +++ b/cmd/mesh-controller/modules_wire_store_test.go @@ -46,15 +46,7 @@ func TestAComposedDeclarationCarriesModulesAndBuildsOnlyForANodeThatReadsThem(t declare := func(node string) sendable { t.Helper() - plan, settings, err := planFor(ctx, open, node) - if err != nil { - t.Fatal(err) - } - gens, err := generators(ctx, open) - if err != nil { - t.Fatal(err) - } - d, err := declarationWith(ctx, open, node, plan, settings, gens, Allocating) + d, err := composeSend(ctx, open, node) if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-controller/modules_wire_test.go b/cmd/mesh-controller/modules_wire_test.go index 518ab17f..7dd25721 100644 --- a/cmd/mesh-controller/modules_wire_test.go +++ b/cmd/mesh-controller/modules_wire_test.go @@ -5,8 +5,7 @@ import ( "fmt" "testing" - "github.com/novox/mesh-controller/internal/chain" - "github.com/novox/mesh-host/validate" + "git.novox.be/novox/mesh-sdk/go/validate" ) // The module of each resource and the build of each module (novox/hq issue 1145, design 47 release 1-2). The @@ -69,19 +68,3 @@ func TestTheNodeEnginesValidatorTakesAModulePerResourceAndABuildPerModule(t *tes t.Fatalf("1145/AC4: refused: %q\n%s", problems, fmt.Sprint(generic)) } } - -// The build a module carries is the one its assignment names when that is the commit carried, else the one built from -// the commit carried; none when neither is known. -func TestTheBuildOfACarriedCommitIsTheAssignmentsOrTheOneBuiltFromIt(t *testing.T) { - runs := map[string]chain.Ref{"a": {Build: "b-a", Commit: "c1"}, "b": {Build: "b-old", Commit: "c0"}} - looked := map[string]string{"b/c2": "b-new"} - got, err := wireBuilds(map[string]string{"a": "c1", "b": "c2", "c": "c3", "d": ""}, runs, - func(module, commit string) (string, error) { return looked[module+"/"+commit], nil }) - if err != nil { - t.Fatal(err) - } - want := map[string]string{"a": "b-a", "b": "b-new"} - if len(got) != len(want) || got["a"] != want["a"] || got["b"] != want["b"] { - t.Fatalf("1145: builds %v, want %v", got, want) - } -} diff --git a/cmd/mesh-controller/moved.go b/cmd/mesh-controller/moved.go index 3e9d95da..f84c23bb 100644 --- a/cmd/mesh-controller/moved.go +++ b/cmd/mesh-controller/moved.go @@ -15,8 +15,8 @@ import ( "git.novox.be/novox/mesh-sdk/go/asks" sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/moved_bus_test.go b/cmd/mesh-controller/moved_bus_test.go index 1e999dd2..e8aebce2 100644 --- a/cmd/mesh-controller/moved_bus_test.go +++ b/cmd/mesh-controller/moved_bus_test.go @@ -12,8 +12,8 @@ import ( "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/testbus" diff --git a/cmd/mesh-controller/network.go b/cmd/mesh-controller/network.go index 283e8335..a05ee537 100644 --- a/cmd/mesh-controller/network.go +++ b/cmd/mesh-controller/network.go @@ -11,9 +11,10 @@ import ( "strings" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" + "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/overlay" ) // the private network: who is on it, where, and what they are called. @@ -308,83 +309,11 @@ func network(ctx context.Context, inv *inventory.Inventory, on map[string]bool, // graph is the whole mesh's network, for showing it. func graph(ctx context.Context, open *stores) ([]overlay.Node, overlay.Graph, error) { - inv := open.inventory - on, refused, err := whoResolves(ctx, open, overlay.Requirement) + in, err := inputFor(ctx, open) if err != nil { return nil, nil, err } - g, err := network(ctx, inv, on, refused) - if err != nil { - return nil, nil, err - } - return g.Nodes(), g.Graph(), nil -} - -// whoResolves is the machines whose resolution answers a requirement, and why the others did not. -// -// By what a module **provides**, not by its name. WireGuard is one way to have a private network -// and there could be others, so a machine is on the network because something it runs provides -// one — asking for a particular module by name would be the mistake this whole mechanism exists -// to avoid. -// -// Resolved rather than read from the assignment table, because a module can arrive by being -// required by something else, and a machine that needs the private network to do its job is on it -// for the same reason as one that was handed it directly. -func whoResolves(ctx context.Context, open *stores, requirement string) ( - map[string]bool, map[string]string, error) { - inv := open.inventory - nodes, err := inv.Nodes(ctx) - if err != nil { - return nil, nil, err - } - on := map[string]bool{} - // Why a node could not be resolved, kept rather than raised: one broken node must not stop - // the rest being described, and whoever is rendering that node will raise it themselves. - refused := map[string]string{} - for _, n := range nodes { - plan, _, err := planFor(ctx, open, n.Name) - switch { - case unresolvable(err): - refused[n.Name] = err.Error() - continue - case err != nil: - // Not a node that does not resolve — a question that went unanswered. Recording it as a - // refusal would take the machine off the private network, and the generator that reads - // this would then write a roster and a filter without it (novox/hq 04-ISSUES/152). - return nil, nil, fmt.Errorf("whether %s answers %q cannot be read: %w", - n.Name, requirement, err) - } - for _, m := range plan.Modules { - for _, offered := range m.Offers() { - if offered == requirement { - on[n.Name] = true - } - } - } - } - return on, refused, nil -} - -// rendering is everything a declaration needs, computed over the whole mesh. -func generators(ctx context.Context, open *stores) ( - map[string]catalogue.Generator, error) { - inv := open.inventory - on, refused, err := whoResolves(ctx, open, overlay.Addressing) - if err != nil { - return nil, err - } - net, err := network(ctx, inv, on, refused) - if err != nil { - return nil, err - } - // **One generator now.** Two more used to sit beside it — the names and a resolver's zone - // file — as modules that ran nothing. Both are facts a module asks for in its manifest - // (`facts:` — catalogue.FactsInto), computed from the same machines this sees: the ones on the - // private network, because a name for a machine not on it would resolve to an address nothing - // can reach. - return map[string]catalogue.Generator{ - overlay.Name: net, - }, nil + return compose.Graph(in.Input) } func overlayShow(ctx context.Context, open *stores) error { @@ -580,68 +509,6 @@ func onTheNetwork(ctx context.Context, inv *inventory.Inventory, return out, nil } -// onThePrivateNetwork is every node's address on the private network, sorted — the same set -// the names and the resolver mean by it (onTheNetwork), so a rule saying "from the mesh" admits -// exactly the machines the mesh names. -// -// A node with no address is left out rather than rendered as an empty source: an empty entry in a -// source set is a syntax error in the rule file, and a rule file that does not load leaves the -// node filtering whatever it was filtering before -- the one outcome worse than a wrong rule, -// because nothing reports it. -func onThePrivateNetwork(ctx context.Context, inv *inventory.Inventory, - shelf map[string]catalogue.Manifest) ([]string, error) { - places, err := onTheNetwork(ctx, inv, shelf) - if err != nil { - return nil, err - } - var out []string - for _, p := range places { - if strings.TrimSpace(p.Address) != "" { - out = append(out, p.Address) - } - } - sort.Strings(out) - return out, nil -} - -// namesInTheMesh is every machine's internal name and the address behind it — every machine -// that is on the private network, the same set the resolver means by that. -// -// A machine that is not has no name: writing one that resolves to nothing is worse than not -// writing it, because a connection to an address that does not answer hangs where a name that -// does not resolve fails at once and says so. A machine placed on the overlay but not running -// the module that puts it there is exactly that (novox/hq issue 079). -func namesInTheMesh(ctx context.Context, inv *inventory.Inventory, - shelf map[string]catalogue.Manifest) (map[string]string, error) { - places, err := onTheNetwork(ctx, inv, shelf) - if err != nil { - return nil, err - } - out := map[string]string{} - for _, p := range places { - out[overlay.InternalName(p.Name)] = p.Address - } - // And the carried peers the operator has named (novox/hq issue 112): machines the - // predecessor's resolver answers for and the mesh routes to, known by name on the operator's - // word until they enrol — at which point enrolment verifies the name and the node's own - // entry takes over above. A name the predecessor answers for must keep resolving until the - // machine behind it is a node; without these, taking the resolver silences three machines. - carried, err := inv.CarriedPeers(ctx) - if err != nil { - return nil, err - } - for _, p := range carried { - if p.Named == "" || p.EnrolledAs != "" { - continue - } - if _, taken := out[overlay.InternalName(p.Named)]; taken { - continue // a node of the mesh owns the name; the stale statement loses - } - out[overlay.InternalName(p.Named)] = p.Address - } - return out, nil -} - // artifactStoreOnNetwork is the machine on this network that offers the artifact store, and the // port THAT MACHINE put it on — the node's setting when it was given one (novox/hq ADR 0100, // 04-ISSUES/102), the mesh's assignment when it made one, and the manifest's own number only when diff --git a/cmd/mesh-controller/network_test.go b/cmd/mesh-controller/network_test.go index de917559..92ae3d31 100644 --- a/cmd/mesh-controller/network_test.go +++ b/cmd/mesh-controller/network_test.go @@ -9,10 +9,10 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/beside" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/overlay" ) // placementOf is what the mesh holds about where one node is. @@ -84,37 +84,6 @@ func TestAPlacementAndNothingTogetherIsRefused(t *testing.T) { } } -// A machine is named for the others only while it is on the private network — placed AND running -// what puts it there — the same set the resolver means by "on the private network". A machine -// that has an address and no networking is not named: a name resolving to an address that does -// not answer hangs where an unknown name fails at once (novox/hq issue 079). -func TestOnlyAMachineOnThePrivateNetworkIsNamed(t *testing.T) { - open := aMesh(t) // two placed machines, both assigned what puts them on the private network - ctx := t.Context() - shelf, err := open.inventory.Catalogue(ctx) - if err != nil { - t.Fatal(err) - } - names, err := namesInTheMesh(ctx, open.inventory, shelf) - if err != nil { - t.Fatal(err) - } - if names["anchor.internal"] != "10.77.0.1" || names["laptop.internal"] != "10.77.0.2" { - t.Fatalf("two machines on the network are not both named: %v", names) - } - // The laptop keeps its place and its address, and stops running the network. - if err := open.inventory.Unassign(ctx, "laptop", overlay.Name); err != nil { - t.Fatal(err) - } - names, err = namesInTheMesh(ctx, open.inventory, shelf) - if err != nil { - t.Fatal(err) - } - if _, still := names["laptop.internal"]; still || names["anchor.internal"] != "10.77.0.1" { - t.Fatalf("a machine that left the network is still named, or the one that stayed is not: %v", names) - } -} - // novox/hq ADR 0105: the range every address is composed from is the adopted tunnel's, read from // the tunnel the hub holds — never stored anywhere else. func TestTheOverlaysRangeIsTheAdoptedTunnels(t *testing.T) { @@ -315,16 +284,8 @@ func TestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves(t *t func TestTheRosterNamesOnlyTheMachines(t *testing.T) { open := aMesh(t) ctx := t.Context() - gens, err := generators(ctx, open) - if err != nil { - t.Fatal(err) - } for _, node := range []string{"anchor", "laptop"} { - plan, settings, err := planFor(ctx, open, node) - if err != nil { - t.Fatal(err) - } - with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Reading) + with, err := renderingOf(ctx, open, node) if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-controller/new_module_approved_test.go b/cmd/mesh-controller/new_module_approved_test.go index 953aebc7..f5293efe 100644 --- a/cmd/mesh-controller/new_module_approved_test.go +++ b/cmd/mesh-controller/new_module_approved_test.go @@ -9,7 +9,7 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/asks" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/new_module_bus_test.go b/cmd/mesh-controller/new_module_bus_test.go index e4579d37..05f65214 100644 --- a/cmd/mesh-controller/new_module_bus_test.go +++ b/cmd/mesh-controller/new_module_bus_test.go @@ -12,8 +12,8 @@ import ( "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/testbus" diff --git a/cmd/mesh-controller/new_module_on_merge_test.go b/cmd/mesh-controller/new_module_on_merge_test.go index 9bd9623d..a022c869 100644 --- a/cmd/mesh-controller/new_module_on_merge_test.go +++ b/cmd/mesh-controller/new_module_on_merge_test.go @@ -6,7 +6,7 @@ import ( "context" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/nodes.go b/cmd/mesh-controller/nodes.go index 8d8e19ec..232bd89e 100644 --- a/cmd/mesh-controller/nodes.go +++ b/cmd/mesh-controller/nodes.go @@ -14,12 +14,12 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" - "github.com/novox/mesh-controller/internal/overlay" "github.com/novox/mesh-controller/internal/token" ) @@ -924,7 +924,7 @@ func showNode(ctx context.Context, stored *stores, name string) error { } // And which of them a push leaves out, and why (novox/hq issue 380): judged as the push judges it, the same // reason its condition says. Not computable is said, never read as "all applied". - plan, settings, err := planFor(ctx, stored, name) + plan, settings, err := resolveNode(ctx, stored, name) switch { case err != nil: fmt.Printf("\n whether a send leaves any assigned module out is NOT known: %s\n", oneLine(err.Error())) diff --git a/cmd/mesh-controller/non_trunk_merge_test.go b/cmd/mesh-controller/non_trunk_merge_test.go index be0ace88..71322750 100644 --- a/cmd/mesh-controller/non_trunk_merge_test.go +++ b/cmd/mesh-controller/non_trunk_merge_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/one_group_one_plan_test.go b/cmd/mesh-controller/one_group_one_plan_test.go index 84c7af6c..14c7647a 100644 --- a/cmd/mesh-controller/one_group_one_plan_test.go +++ b/cmd/mesh-controller/one_group_one_plan_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/one_resolution_integration_test.go b/cmd/mesh-controller/one_resolution_integration_test.go index 989f3ca2..f8972224 100644 --- a/cmd/mesh-controller/one_resolution_integration_test.go +++ b/cmd/mesh-controller/one_resolution_integration_test.go @@ -5,9 +5,11 @@ package main import ( "bytes" "context" - "errors" "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/compose" ) func TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer(t *testing.T) { @@ -23,7 +25,7 @@ func TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer(t *testing.T said := answered(t, func() error { return assignCommand(ctx, "unassign", []string{"laptop", "notes"}) }) - if _, _, err := planFor(ctx, open, "laptop"); err == nil || !unresolvable(err) { + if _, _, err := resolveNode(ctx, open, "laptop"); err == nil || !unresolvable(err) { t.Fatalf("laptop resolves with two claimants of one seat: %v", err) } if n := strings.Count(said, "laptop does not resolve now"); n != 1 { @@ -45,7 +47,7 @@ func TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt(t *testing.T) { if err != nil { t.Fatal(err) } - if _, _, err := planFor(ctx, open, "laptop"); err != nil { + if _, _, err := resolveNode(ctx, open, "laptop"); err != nil { t.Fatal(err) } if strings.Contains(said, "does not resolve") || strings.Contains(said, "could not be read") { @@ -65,7 +67,7 @@ func TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal(t *testing.T if err == nil { t.Fatalf("two claimants of one node seat resolved:\n%s", said) } - _, _, planErr := planFor(ctx, open, "laptop") + _, _, planErr := resolveNode(ctx, open, "laptop") if planErr == nil || planErr.Error() != err.Error() { t.Errorf("the act's refusal is not the plan's right after:\nact: %v\nplan: %v", err, planErr) } @@ -75,15 +77,16 @@ func TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal(t *testing.T } // The other readers of the whole mesh still say a machine they leave out, on standard error (novox/hq -// issue 188); an act sends it elsewhere. +// issue 188); an act sends it elsewhere. The line is mesh-sdk's (compose.Declared.Said); where it goes is the context's. func TestAMachineLeftOutIsSaidWhereTheContextSays(t *testing.T) { + line := "laptop is left out of the rest of the mesh: it does not resolve: why" var buf bytes.Buffer - sayLeftOut(leftOutSaidBy(context.Background(), &buf), "laptop", errors.New("why")) - if buf.String() != "laptop is left out of the rest of the mesh: it does not resolve: why\n" { + sayFound(leftOutSaidBy(context.Background(), &buf), compose.Declared{Said: []string{line}}) + if buf.String() != line+"\n" { t.Errorf("said %q", buf.String()) } got := answered(t, func() error { - sayLeftOut(context.Background(), "laptop", errors.New("why")) + sayFound(context.Background(), compose.Declared{Said: []string{line}}) return nil }) if !strings.Contains(got, "laptop is left out") { diff --git a/cmd/mesh-controller/one_resolution_test.go b/cmd/mesh-controller/one_resolution_test.go index 82a873a9..406b9ec7 100644 --- a/cmd/mesh-controller/one_resolution_test.go +++ b/cmd/mesh-controller/one_resolution_test.go @@ -3,7 +3,7 @@ package main import ( "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" ) // novox/hq issue 237: every line of an act's answer comes from one resolution of the node taken after diff --git a/cmd/mesh-controller/operator_wait.go b/cmd/mesh-controller/operator_wait.go index 48e8bae4..6de6747b 100644 --- a/cmd/mesh-controller/operator_wait.go +++ b/cmd/mesh-controller/operator_wait.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" diff --git a/cmd/mesh-controller/operator_wait_test.go b/cmd/mesh-controller/operator_wait_test.go index 1961f821..15879fa7 100644 --- a/cmd/mesh-controller/operator_wait_test.go +++ b/cmd/mesh-controller/operator_wait_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" diff --git a/cmd/mesh-controller/order_test.go b/cmd/mesh-controller/order_test.go index f362c8df..9bd36d56 100644 --- a/cmd/mesh-controller/order_test.go +++ b/cmd/mesh-controller/order_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/outside_accept_test.go b/cmd/mesh-controller/outside_accept_test.go index bf314b1c..f34f8d58 100644 --- a/cmd/mesh-controller/outside_accept_test.go +++ b/cmd/mesh-controller/outside_accept_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" ) // One accept for a provider reaches every consumer that binds the provision from it, and each consumer's send @@ -32,7 +32,7 @@ func TestOneAcceptForAProviderIsPlacedForEveryConsumer(t *testing.T) { } placed := func(module string) bool { t.Helper() - plan, settings, err := planFor(ctx, open, "laptop") + plan, settings, err := resolveNode(ctx, open, "laptop") if err != nil { t.Fatalf("laptop was refused: %v", err) } diff --git a/cmd/mesh-controller/outside_not_given_test.go b/cmd/mesh-controller/outside_not_given_test.go index 74421aa9..c38ae56e 100644 --- a/cmd/mesh-controller/outside_not_given_test.go +++ b/cmd/mesh-controller/outside_not_given_test.go @@ -5,7 +5,8 @@ package main import ( "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/compose" ) // An own secret issued outside the mesh that nobody gave is never made (novox/hq issue 401, ADR 0228). Seen on @@ -28,7 +29,7 @@ func anAssistant() catalogue.Manifest { // sentTo composes what a push sends to a machine, on the send path. func sentTo(t *testing.T, open *stores, node string) []map[string]any { t.Helper() - plan, settings, err := planFor(t.Context(), open, node) + plan, settings, err := resolveNode(t.Context(), open, node) if err != nil { t.Fatal(err) } @@ -99,16 +100,8 @@ func TestAQuestionComposesWithoutAnOutsideSecretNobodyGave(t *testing.T) { t.Fatal(err) } pushedBy(t, open, "laptop") // the mesh's own secret is made by the send - plan, settings, err := planFor(ctx, open, "laptop") - if err != nil { - t.Fatal(err) - } - gens, err := generators(ctx, open) - if err != nil { - t.Fatal(err) - } - for _, choosing := range []Choosing{Reading, Foreseeing} { - got, err := declarationWith(ctx, open, "laptop", plan, settings, gens, choosing) + for _, choosing := range []compose.Mode{compose.Reading, compose.Foreseeing} { + got, _, err := composeQuestion(ctx, open, "laptop", choosing, false) if err != nil { t.Fatalf("%v: %v", choosing, err) } diff --git a/cmd/mesh-controller/outside_wait_test.go b/cmd/mesh-controller/outside_wait_test.go index 1c7a76b8..f5f344a9 100644 --- a/cmd/mesh-controller/outside_wait_test.go +++ b/cmd/mesh-controller/outside_wait_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" diff --git a/cmd/mesh-controller/over_budget.go b/cmd/mesh-controller/over_budget.go index a3b8101e..03c578ff 100644 --- a/cmd/mesh-controller/over_budget.go +++ b/cmd/mesh-controller/over_budget.go @@ -9,7 +9,7 @@ import ( "github.com/nats-io/nats.go" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" diff --git a/cmd/mesh-controller/over_budget_integration_test.go b/cmd/mesh-controller/over_budget_integration_test.go index 38fc57ab..c16b24ad 100644 --- a/cmd/mesh-controller/over_budget_integration_test.go +++ b/cmd/mesh-controller/over_budget_integration_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/over_budget_test.go b/cmd/mesh-controller/over_budget_test.go index 52773bbc..9ba16ee4 100644 --- a/cmd/mesh-controller/over_budget_test.go +++ b/cmd/mesh-controller/over_budget_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/own_path_by_claim_test.go b/cmd/mesh-controller/own_path_by_claim_test.go index a54bfd44..0b02f7ac 100644 --- a/cmd/mesh-controller/own_path_by_claim_test.go +++ b/cmd/mesh-controller/own_path_by_claim_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) diff --git a/cmd/mesh-controller/owned_paths_test.go b/cmd/mesh-controller/owned_paths_test.go index 10784a11..1aa8aac4 100644 --- a/cmd/mesh-controller/owned_paths_test.go +++ b/cmd/mesh-controller/owned_paths_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/novox/mesh-host/validate" + "git.novox.be/novox/mesh-sdk/go/validate" ) // A directory or an archive that says how it reacts to files it does not know (novox/hq ADR 0422) is a diff --git a/cmd/mesh-controller/pending_integration_test.go b/cmd/mesh-controller/pending_integration_test.go index ba6a82fe..c6c5b90d 100644 --- a/cmd/mesh-controller/pending_integration_test.go +++ b/cmd/mesh-controller/pending_integration_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) diff --git a/cmd/mesh-controller/pending_test.go b/cmd/mesh-controller/pending_test.go index c5171bca..ed5c12ee 100644 --- a/cmd/mesh-controller/pending_test.go +++ b/cmd/mesh-controller/pending_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" diff --git a/cmd/mesh-controller/person_wait_test.go b/cmd/mesh-controller/person_wait_test.go index cd61536e..eeb3a77d 100644 --- a/cmd/mesh-controller/person_wait_test.go +++ b/cmd/mesh-controller/person_wait_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" diff --git a/cmd/mesh-controller/placement_moved_test.go b/cmd/mesh-controller/placement_moved_test.go index f4a8938b..661f0691 100644 --- a/cmd/mesh-controller/placement_moved_test.go +++ b/cmd/mesh-controller/placement_moved_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" ) // **Placement leaves the settings layers and no machine is told anything new** (novox/hq ADR 0348 clause @@ -63,16 +63,12 @@ func TestADeclarationIsTheSameAfterPlacementMoves(t *testing.T) { } } - gens, err := generators(ctx, open) - if err != nil { - t.Fatal(err) - } for _, node := range []string{"anchor", "laptop"} { - plan, settings, err := planFor(ctx, open, node) + plan, _, err := resolveNode(ctx, open, node) if err != nil { t.Fatal(err) } - with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Allocating) + with, err := madeAndRendered(ctx, open, node) if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-controller/placement_words_moved_integration_test.go b/cmd/mesh-controller/placement_words_moved_integration_test.go index f707f74d..75e3b000 100644 --- a/cmd/mesh-controller/placement_words_moved_integration_test.go +++ b/cmd/mesh-controller/placement_words_moved_integration_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/novox/mesh-controller/internal/catalogue" + "git.novox.be/novox/mesh-sdk/go/catalogue" ) func aMeshWithTheWordsInItsLayers(t *testing.T) (*stores, wordedMesh) { diff --git a/cmd/mesh-controller/placement_words_moved_test.go b/cmd/mesh-controller/placement_words_moved_test.go index f6562433..3a8dbadc 100644 --- a/cmd/mesh-controller/placement_words_moved_test.go +++ b/cmd/mesh-controller/placement_words_moved_test.go @@ -49,15 +49,11 @@ func theWordedMesh(t *testing.T) wordedMesh { func toldOfTheWords(t *testing.T, open *stores, node string) string { t.Helper() ctx := t.Context() - gens, err := generators(ctx, open) + plan, _, err := resolveNode(ctx, open, node) if err != nil { t.Fatal(err) } - plan, settings, err := planFor(ctx, open, node) - if err != nil { - t.Fatal(err) - } - with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Allocating) + with, err := madeAndRendered(ctx, open, node) if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-controller/plan.go b/cmd/mesh-controller/plan.go index 108c6d83..bd0a70fe 100644 --- a/cmd/mesh-controller/plan.go +++ b/cmd/mesh-controller/plan.go @@ -14,13 +14,10 @@ import ( "strings" "sync" + "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/catalogue" - "github.com/novox/mesh-controller/internal/chain" "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/licences" "github.com/novox/mesh-controller/internal/link" - "github.com/novox/mesh-controller/internal/overlay" ) // working out what one machine should be. @@ -55,339 +52,6 @@ func unresolvable(err error) bool { return errors.As(err, &n) } -// planFor works out everything a node should run, from what was assigned to it. -// -// A failure to compose the node's own modules is wrapped as notResolvable; every other failure is -// returned as it is. Callers gathering across the mesh must tell them apart — see notResolvable. -func planFor(ctx context.Context, open *stores, nodeName string) (catalogue.Resolution, catalogue.SettingsBy, error) { - inv := open.inventory - shelf, err := inv.Catalogue(ctx) - if err != nil { - return catalogue.Resolution{}, nil, err - } - assigned, err := inv.Assigned(ctx, nodeName) - if err != nil { - return catalogue.Resolution{}, nil, err - } - capabilities, err := inv.ProfileOf(ctx, nodeName) - if err != nil { - return catalogue.Resolution{}, nil, err - } - - places, err := inv.Overlays(ctx) - if err != nil { - return catalogue.Resolution{}, nil, err - } - var site string - for _, p := range places { - if p.Name == nodeName { - site = p.Site - } - } - - world, err := theRestOfTheMesh(ctx, inv, shelf, nodeName) - if err != nil { - return catalogue.Resolution{}, nil, err - } - world.Pinned, err = inv.PinsFor(ctx, nodeName) - if err != nil { - return catalogue.Resolution{}, nil, err - } - // Where each of its consumers of a provision that keeps data was last sent (novox/hq ADR 0232): - // a resolution that would answer one from anywhere else keeps it there, and says so. - world.Bound, err = inv.BindingsFor(ctx, nodeName) - if err != nil { - return catalogue.Resolution{}, nil, err - } - - onNetwork, err := whereEveryoneIs(ctx, inv, shelf) - if err != nil { - return catalogue.Resolution{}, nil, err - } - - // What this mesh can answer with a record rather than a machine, and which record each of - // this node's modules was put on. Read across a context boundary by name, which is what - // crossing one is allowed to carry (novox/hq ADR 0008). - world.Licences, world.Using, err = licencesFor(ctx, open, nodeName) - if err != nil { - return catalogue.Resolution{}, nil, err - } - - // The domain this node composes its routed names under (novox/hq ADR 0066). A route - // contribution carries only a label; the resolver joins