From 2111b78fc5b5ed5b46e2eda19d2802c6c1773a97 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 11:18:42 +0000 Subject: [PATCH] Delete the controller's own composition: planFor, declarationWith, renderingFor, grantsFor, theRestOfTheMesh, generators, whoResolves and composedAndValidated, with what only they reached (issue 1389) Every caller composes through mesh-sdk's go/compose since issues 1386 to 1388; the controller's second copy of the composition is dead code. The parent of this commit is fb556d6b, whose try (build-1792493691274712799, 17m28s, 2026-10-20 11:12 UTC) ran the equality test green on the live facts: every machine was sent and compared both ways (ADR 0453, issue 1384). Deleted, 40 functions: in plan.go planFor, declarationWith, renderingFor, grantsFor, theRestOfTheMesh and their helpers (boundOfGrant, busCredentialIssued, carriedBuilds, carriedOn, composeBusUsers, engineJudgesRoot, engineReadsHealth, engineTakesHanded, takesHanded, givenUnderAnOldKey, issuedToHolder, keyFor, licencesFor, managerPublicKeyFor, movedProvider, providerMoved, providerModuleOf, sayLeftOut, seatsOn, wireBuilds, zonesInTheMesh); in network.go generators, whoResolves, namesInTheMesh, onThePrivateNetwork; composedAndValidated, adoptionFor, adoptionOf, engineOpensVaultBox, namedModules, localNetworksOf, keepRecorded, passSeatsOn, theMeshPassesSeatsOn, assignmentsOf. Choosing (Reading, Allocating, Foreseeing) is gone: coreBoxesFor and coreBoxesOf take a plain allocating flag. composeForPush no longer takes the private network it ignored. The seat pass of a send is performSeatPasses, over the passes the composition names. Measured: cmd/mesh-controller non-test code 67,643 -> 65,821 lines (-1,822), its tests 57,691 -> 57,286, with the integration part of the equality test removed here because it calls the deleted functions. Not the issue's 3,000: that counts all of plan.go (2,192 lines), of which the callers, the verbs, the perform step and the sends remain (sendable, recorded builds, the held-back and ready-node logic, the plan verb, the gate's trial); and the 37 functions a name walk lists as unreachable are called through interfaces and method sets and are kept. Tests dropped, each with the mesh-sdk go/compose test that holds its cases: - seat_pass_test.go TestACompositionWithTwoClaimantsRecordsNoneAndSaysSo, TestACompositionChangesNothingWhileTheHolderIsAssignedOrNobodyClaims, TestAHolderAssignedButNotOnTheShelfIsNotTakenForGone: rest_test.go TestNoSeatIsPassedForTwoClaimantsAnAssignedHolderOrNoClaimant, TestAHolderAssignedButNotOnTheShelfIsNotTakenForGone, TestASeatWhoseHolderIsAssignedNowhereGoesToItsOnlyClaimant. TestACompositionPassesTheSeatAndWritesOneHandActByTheController and TestALostPassWritesNoActAndAFailedOneLeavesTheRecord stay as tests of the send's step (performSeatPasses); TestAThrowawayMeshPassesTheSeatInMemoryAndWritesNothing stays as TestAThrowawayMeshWritesNoHandActForAPass. - seat_pass_test.go TestNoSeatIsPassedWhenAMachinesAssignmentsCouldNotBeRead: no counterpart, and none is needed: the adapter reads every machine's assignments for the Input and a machine whose read fails fails the Input, so no seat is passed on a partial read. - network_test.go TestOnlyAMachineOnThePrivateNetworkIsNamed: network_test.go TestANodeNotOnThePrivateNetworkGetsNoAddressAndNoNetwork. - held_back_test.go TestASendCarriesTheCurrentBuildsAndALeftOutModuleKeepsItsOwn: node_test.go TestAModuleLeftOutKeepsTheBuildItWasLastSent (and compose_sdk_test.go's table, now literal). - modules_wire_test.go TestTheBuildOfACarriedCommitIsTheAssignmentsOrTheOneBuiltFromIt: node_test.go TestTheBodyCarriesTheOrderTheInputGivesAndTheBuildsOnlyToAnEngineThatReadsThem. - compose_equal_integration_test.go: the old path it compares against is deleted; its last run is the parent's try. Rewritten against mesh-sdk, same cases: issued_test.go (needs now come from compose.Resolve: Needed.IssuedToHolder and ProviderMoved), module_health_test.go and merge_gate_handing tests (the contract in the Input), provisioner_account_integration_test.go (the composition's refusal), one_resolution_integration_test.go (sayFound). Every test of the send, perform, the adapter and the verbs is kept; the tests that used planFor, declarationWith, generators, renderingFor or grantsFor as setup use resolveNode, composeSend, composeQuestion, renderingOf and grantsOf. compose_callers_test.go now fails when any deleted name is declared or called again. Committed by copying the files of a staging branch (stage/r4b-1389 at 198be8217c67, built from fb556d6b by patch commits and compared with the author's worktree), so that this commit's parent is fb556d6b. --- cmd/mesh-controller/adoption.go | 11 +- .../awaiting_push_integration_test.go | 13 +- cmd/mesh-controller/awaiting_push_test.go | 7 +- .../bindings_integration_test.go | 6 +- cmd/mesh-controller/compose_callers_test.go | 38 +- .../compose_equal_integration_test.go | 310 ---- cmd/mesh-controller/compose_helpers_test.go | 33 + ...pose_input_credentials_integration_test.go | 2 +- .../compose_input_integration_test.go | 4 +- .../compose_made_integration_test.go | 46 + cmd/mesh-controller/compose_perform.go | 22 +- .../compose_perform_integration_test.go | 2 +- cmd/mesh-controller/compose_sdk.go | 18 + cmd/mesh-controller/compose_sdk_test.go | 34 +- cmd/mesh-controller/config_moved_test.go | 8 +- cmd/mesh-controller/core_box.go | 24 +- .../core_box_integration_test.go | 2 +- cmd/mesh-controller/core_box_test.go | 12 +- .../credential_on_assign_test.go | 2 +- cmd/mesh-controller/epoch_send_test.go | 12 +- cmd/mesh-controller/facts.go | 37 - cmd/mesh-controller/facts_test.go | 6 +- cmd/mesh-controller/generation.go | 13 - .../grant_follows_binding_integration_test.go | 18 +- cmd/mesh-controller/grants_step_store_test.go | 8 +- cmd/mesh-controller/held_back.go | 7 +- .../held_back_integration_test.go | 12 +- cmd/mesh-controller/held_back_test.go | 17 - cmd/mesh-controller/holdings_test.go | 2 +- .../identity_bound_integration_test.go | 6 +- .../issue201_integration_test.go | 2 +- cmd/mesh-controller/issued_test.go | 112 +- cmd/mesh-controller/local_network.go | 18 - .../merge_gate_handing_integration_test.go | 9 +- .../merge_gate_handing_test.go | 6 +- cmd/mesh-controller/module_health_test.go | 6 +- .../modules_wire_store_test.go | 10 +- cmd/mesh-controller/modules_wire_test.go | 17 - cmd/mesh-controller/network.go | 129 -- cmd/mesh-controller/network_test.go | 41 +- .../one_resolution_integration_test.go | 19 +- cmd/mesh-controller/outside_accept_test.go | 2 +- cmd/mesh-controller/outside_not_given_test.go | 15 +- cmd/mesh-controller/placement_moved_test.go | 8 +- .../placement_words_moved_test.go | 8 +- cmd/mesh-controller/plan.go | 1494 +---------------- .../provisioner_account_integration_test.go | 8 +- cmd/mesh-controller/push.go | 6 +- cmd/mesh-controller/push_recreates_test.go | 10 +- cmd/mesh-controller/recorded_kept.go | 10 - .../recorded_kept_integration_test.go | 14 +- cmd/mesh-controller/recorded_moves_test.go | 2 +- cmd/mesh-controller/replay164_test.go | 2 +- cmd/mesh-controller/replay201_test.go | 2 +- cmd/mesh-controller/replay237_test.go | 2 +- cmd/mesh-controller/replay401_test.go | 2 +- .../replays_integration_test.go | 4 +- cmd/mesh-controller/roster_failure_test.go | 4 +- cmd/mesh-controller/seat_pass.go | 90 +- cmd/mesh-controller/seat_pass_test.go | 126 +- cmd/mesh-controller/sendable.go | 53 - .../sendable_integration_test.go | 6 +- cmd/mesh-controller/sendable_test.go | 2 +- .../sent_required_by_integration_test.go | 2 +- cmd/mesh-controller/served_own_names_test.go | 4 +- cmd/mesh-controller/unbound_test.go | 4 +- cmd/mesh-controller/vault_switch_test.go | 2 +- 67 files changed, 378 insertions(+), 2605 deletions(-) delete mode 100644 cmd/mesh-controller/compose_equal_integration_test.go create mode 100644 cmd/mesh-controller/compose_helpers_test.go create mode 100644 cmd/mesh-controller/compose_made_integration_test.go diff --git a/cmd/mesh-controller/adoption.go b/cmd/mesh-controller/adoption.go index 96390916..0ff12403 100644 --- a/cmd/mesh-controller/adoption.go +++ b/cmd/mesh-controller/adoption.go @@ -13,7 +13,6 @@ import ( "time" "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" ) @@ -708,18 +707,10 @@ func converge(ctx context.Context, open *stores, node string, yes bool, digest s } // What the filter is derived from: the rendering of this machine as a question reads it, with the private network. - input, err := inputFor(ctx, open) + with, err := renderingOf(ctx, open, node) if err != nil { return "", err } - rendered, err := compose.Render(input.Input, node, compose.Reading) - if err != nil { - return "", err - } - with := rendered.Rendering - if with.Generators, err = compose.Generators(input.Input); err != nil { - return "", err - } rules, err := plan.Rules(with) if err != nil { return "", err diff --git a/cmd/mesh-controller/awaiting_push_integration_test.go b/cmd/mesh-controller/awaiting_push_integration_test.go index 3cf81782..b18d766f 100644 --- a/cmd/mesh-controller/awaiting_push_integration_test.go +++ b/cmd/mesh-controller/awaiting_push_integration_test.go @@ -9,6 +9,7 @@ import ( "time" "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" @@ -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 8c947f3d..74b7097b 100644 --- a/cmd/mesh-controller/awaiting_push_test.go +++ b/cmd/mesh-controller/awaiting_push_test.go @@ -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/bindings_integration_test.go b/cmd/mesh-controller/bindings_integration_test.go index f7571e26..da08a10a 100644 --- a/cmd/mesh-controller/bindings_integration_test.go +++ b/cmd/mesh-controller/bindings_integration_test.go @@ -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/compose_callers_test.go b/cmd/mesh-controller/compose_callers_test.go index 4cd076c7..626860dd 100644 --- a/cmd/mesh-controller/compose_callers_test.go +++ b/cmd/mesh-controller/compose_callers_test.go @@ -10,21 +10,13 @@ import ( "testing" ) -// 1388: no caller composes through the controller's own composition any more. The only functions left that call it are the -// composition's own, which issue 1389 deletes with it; this fails the moment any other function reaches for it again. -func TestNoCallerComposesThroughTheControllersOwnComposition(t *testing.T) { - legacy := map[string]bool{"planFor": true, "declarationWith": true, "composedAndValidated": true, "generators": true, - "renderingFor": true, "theRestOfTheMesh": true, "whoResolves": true} - // The composition's own pairs, until 1389: "file: caller calls callee". - allowed := map[string]bool{ - "facts.go: composedAndValidated calls declarationWith": true, - "facts.go: composedAndValidated calls planFor": true, - "network.go: generators calls whoResolves": true, - "network.go: whoResolves calls planFor": true, - "plan.go: declarationWith calls renderingFor": true, - "plan.go: grantsFor calls planFor": true, - "plan.go: planFor calls theRestOfTheMesh": true, - } +// 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) @@ -41,12 +33,18 @@ func TestNoCallerComposesThroughTheControllersOwnComposition(t *testing.T) { } for _, d := range file.Decls { fn, ok := d.(*ast.FuncDecl) - if !ok || fn.Body == nil { + 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 && legacy[id.Name] { + if id, ok := c.Fun.(*ast.Ident); ok && gone[id.Name] { found = append(found, f+": "+fn.Name.Name+" calls "+id.Name) } } @@ -55,9 +53,7 @@ func TestNoCallerComposesThroughTheControllersOwnComposition(t *testing.T) { } } sort.Strings(found) - for _, pair := range found { - if !allowed[pair] { - t.Errorf("%s: it composes through the controller's own composition, not mesh-sdk's", pair) - } + 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_equal_integration_test.go b/cmd/mesh-controller/compose_equal_integration_test.go deleted file mode 100644 index 335645ca..00000000 --- a/cmd/mesh-controller/compose_equal_integration_test.go +++ /dev/null @@ -1,310 +0,0 @@ -//go:build integration - -package main - -import ( - "context" - "flag" - "fmt" - "os" - "sort" - "strings" - "testing" - "time" - - "git.novox.be/novox/mesh-sdk/go/compose" - snapshot "git.novox.be/novox/mesh-sdk/go/facts" -) - -// 1384: nothing shows the controller's composition and mesh-sdk's compose every node alike on the live facts. This test is -// that proof (novox/hq ADR 0453 clause 3): it reads the facts snapshot MESH_FACTS names (the live one in every check and -// every try, the build seat gives it), else one captured in testdata/facts-equal.json (taken with `facts show` and -// scrubbed as the snapshot is), else the snapshot of a small mesh of its own; raises it into throwaway stores as the merge -// gate does; and composes every machine both ways. -// -// It runs only when named (a try with this test's name as the tests pattern); a whole-suite run skips it. -// -// (a) Foreseeing on the fresh store: the same error or none, the same validator problems, LeftOut and its reasons, Withheld, -// Unbound, Unplaced, HandedOnly and the own secrets named foreseen; and the ports mesh-sdk names foreseen are the ones the -// controller allocates when it sends. -// (b) the send: on two stores raised from the same snapshot, the controller's Allocating composition of every machine, and the -// adapter's compose, perform and compose again; the same ports, pair credentials and own secrets were made, by key. -// (c) after (b), Reading on one store, both ways: the declaration byte for byte. -// -// It sends nothing and opens no store but the throwaway ones. A difference names the machine, the comparison and the first -// field that differs; a machine both paths refuse alike is said, not failed. The test is deleted with the controller's -// composition (issue 1389). -func TestEveryMachineComposesAlikeBothWays(t *testing.T) { - // It is slow (it composes every machine of the live mesh several times each way) and is deleted with the controller's - // composition (issue 1389), so it runs only when asked for by name: a try or a `go test -run` that names it. A merge check - // runs every test and does not. - if run := flag.Lookup("test.run"); run == nil || !strings.Contains(run.Value.String(), "TestEveryMachineComposesAlikeBothWays") { - t.Skip("runs only when named: a try with the tests pattern TestEveryMachineComposesAlikeBothWays") - } - ctx := t.Context() - admin := os.Getenv("MESH_TEST_POSTGRES") - if admin == "" { - t.Skip("no throwaway PostgreSQL (MESH_TEST_POSTGRES) to raise the mesh in") - } - started := time.Now() - phase := func(what string) { t.Logf("[%s] %s", time.Since(started).Round(time.Second), what) } - f, source := equalityFacts(t) - if len(f.Machines) < 1 { - t.Fatalf("the facts (%s) hold no machine, so nothing was composed and nothing is proved", source) - } - t.Logf("the facts of %s (%s): %d machine(s)", f.Taken.Format("2006-01-02 15:04:05"), source, len(f.Machines)) - shelf, err := shelfOfFacts(f) - if err != nil { - t.Fatal(err) - } - sources := sourcesOfFacts(f) - - // Three stores, raised from the same snapshot, one after the other: (a) both ways on a fresh store, (b) and (c) the - // controller's sends and readings, (b) mesh-sdk's sends. They cannot run together: throwawayStores finds its databases by - // the process environment (and a store opened later finds them there too), so a second one raised at the same time takes - // the first one's connection from under it (the live try: database gate_inventory_... does not exist). - var ( - oldForeseen, sdkForeseen = map[string]composedBoth{}, map[string]composedBoth{} - foreseenPorts = map[string][]string{} - readOld, readSDK = map[string]composedBoth{}, map[string]composedBoth{} - oldMade, sdkMade made - oldRefused = map[string]string{} - sendRefused = map[string]string{} - errA, errOld, errSDK error - ) - func() { // (a) Foreseeing on the fresh store. - phase("(a) starts: raising a fresh store") - fresh, drop, err := throwawayStores(ctx, admin) - if err != nil { - errA = err - return - } - defer drop() - if _, err := raiseFromFacts(ctx, fresh, f, shelf, sources); err != nil { - errA = err - return - } - // mesh-sdk's composition first: it writes nothing, where the controller's makes every pair credential on the way. The - // Input is read once for all of its machines. - phase("(a) the store is raised; mesh-sdk's Foreseeing starts") - cached := withInputCache(ctx) - for _, m := range f.Machines { - in, err := inputFor(cached, fresh) - if err != nil { - errA = fmt.Errorf("%s: the adapter cannot read the mesh: %w", m.Name, err) - return - } - d, problems, cerr := compose.ComposeIn(in.Input, m.Name, compose.Foreseeing) - if cerr == nil { - foreseenPorts[m.Name] = append([]string(nil), d.ForeseenPorts...) - sort.Strings(foreseenPorts[m.Name]) - } - sdkForeseen[m.Name] = capturedFrom(sendableOf(d), problems, cerr) - } - phase("(a) mesh-sdk's Foreseeing of every machine") - gens, gensErr := generators(ctx, fresh) - for _, m := range f.Machines { - if gensErr != nil { - oldForeseen[m.Name] = composedBoth{Err: gensErr.Error()} - continue - } - d, problems, err := composedAndValidated(ctx, fresh, m.Name, gens, Foreseeing) - oldForeseen[m.Name] = capturedFrom(d, problems, err) - } - phase("(a) the controller's Foreseeing of every machine") - }() - if errA != nil { - t.Fatal(errA) - } - func() { // (b) and (c): the controller's send of every machine, then both ways of reading. - phase("(b) starts: the controller's send, raising a store") - oldStore, drop, err := throwawayStores(ctx, admin) - if err != nil { - errOld = err - return - } - defer drop() - if _, err := raiseFromFacts(ctx, oldStore, f, shelf, sources); err != nil { - errOld = err - return - } - oldGens, oldGensErr := generators(ctx, oldStore) - for _, m := range f.Machines { - if oldGensErr != nil { - oldRefused[m.Name] = oldGensErr.Error() - continue - } - if _, _, err := composedAndValidated(ctx, oldStore, m.Name, oldGens, Allocating); err != nil { - oldRefused[m.Name] = err.Error() - } - } - phase("(b) the controller's send of every machine") - if oldMade, err = madeIn(ctx, oldStore); err != nil { - errOld = err - return - } - cached := withInputCache(ctx) - for _, m := range f.Machines { - if oldGensErr != nil { - readOld[m.Name] = composedBoth{Err: oldGensErr.Error()} - } else { - d, problems, err := composedAndValidated(ctx, oldStore, m.Name, oldGens, Reading) - readOld[m.Name] = capturedFrom(d, problems, err) - } - d, problems, err := composeQuestion(cached, oldStore, m.Name, compose.Reading, true) - readSDK[m.Name] = capturedFrom(d, problems, err) - } - phase("(c) both ways of reading every machine after the controller's send") - }() - if errOld != nil { - t.Fatal(errOld) - } - func() { // (b) mesh-sdk's send of every machine. - phase("(b) starts: mesh-sdk's send, raising a store") - sdkStore, drop, err := throwawayStores(ctx, admin) - if err != nil { - errSDK = err - return - } - defer drop() - if _, err := raiseFromFacts(ctx, sdkStore, f, shelf, sources); err != nil { - errSDK = err - return - } - for _, m := range f.Machines { - if _, err := composeSend(ctx, sdkStore, m.Name); err != nil { - sendRefused[m.Name] = err.Error() - } - } - phase("(b) mesh-sdk's send of every machine") - sdkMade, errSDK = madeIn(ctx, sdkStore) - }() - if errSDK != nil { - t.Fatal(errSDK) - } - - var failures []string - for _, m := range f.Machines { - failures = append(failures, bothWaysDiffer(m.Name, "(a) Foreseeing", oldForeseen[m.Name], sdkForeseen[m.Name], false)...) - failures = append(failures, bothWaysDiffer(m.Name, "(c) Reading after the send", readOld[m.Name], readSDK[m.Name], true)...) - } - // **The send must compose every machine both ways.** A machine either send refuses is no proof of anything: what the - // controller made on the way to refusing it is no send's, and a refusal alike says nothing about what a send of it makes. - // So it fails here, naming the machine and both refusals (a refusal alike in (a), the Foreseeing on a store that holds no - // secret a send made, is said and stays). A pass proves that every machine of the facts was sent and compared. - refused := map[string]bool{} - for _, m := range f.Machines { - why, oldNo := oldRefused[m.Name] - sdkWhy, sdkNo := sendRefused[m.Name] - if !oldNo && !sdkNo { - continue - } - refused[m.Name] = true - failures = append(failures, fmt.Sprintf("%s, (b) the send: it is not composed both ways, so nothing of it is compared: "+ - "the controller's refusal %s, mesh-sdk's %s", m.Name, quoted(oneLine(why)), quoted(oneLine(sdkWhy)))) - } - failures = append(failures, madeDiffer("(b) the send", oldMade, sdkMade, refused)...) - - // (a)'s ports: those mesh-sdk names foreseen, for each machine, are the ones the controller's send allocated to it. - for _, m := range f.Machines { - if refused[m.Name] { - continue - } - var allocated []string - for _, p := range oldMade.Ports { - if node, rest, ok := strings.Cut(p, " "); ok && node == m.Name { - module, port, _ := strings.Cut(strings.SplitN(rest, "->", 2)[0], ":") - allocated = append(allocated, module+":"+port) - } - } - sort.Strings(allocated) - // A port the snapshot already holds is not allocated by a send, and one a module is given is not allocated either: the - // ones mesh-sdk names must all be among those the send allocated. - for _, want := range foreseenPorts[m.Name] { - found := false - for _, got := range allocated { - found = found || got == want - } - if !found { - failures = append(failures, fmt.Sprintf("%s, (a) Foreseeing: mesh-sdk names the port %s foreseen and the "+ - "controller's send allocates none for it", m.Name, want)) - } - } - } - for machine, why := range sendRefused { - t.Logf("%s: mesh-sdk's send of it is refused: %s", machine, why) - } - for machine, c := range oldForeseen { - if c.Err != "" { - t.Logf("%s: the controller refuses it, and mesh-sdk alike: %s", machine, oneLine(c.Err)) - } - } - if len(failures) > 0 { - t.Fatalf("the two compositions differ on %d point(s):\n %s", len(failures), strings.Join(failures, "\n ")) - } - t.Logf("%d machine(s) were sent and compose alike both ways", len(f.Machines)) -} - -// equalityFacts is the snapshot the comparison composes: MESH_FACTS, else the one captured in testdata, else the facts of a -// mesh of its own (the same shape, small). Source says which. -func equalityFacts(t *testing.T) (snapshot.Facts, string) { - t.Helper() - ctx := t.Context() - if path := os.Getenv("MESH_FACTS"); path != "" { - f, err := readFacts(ctx, path) - if err != nil { - t.Fatalf("MESH_FACTS names %s and it is not a snapshot: %v", path, err) - } - return f, "MESH_FACTS " + path - } - if _, err := os.Stat("testdata/facts-equal.json"); err == nil { - f, err := readFacts(ctx, "testdata/facts-equal.json") - if err != nil { - t.Fatal(err) - } - return f, "captured in testdata/facts-equal.json" - } - open, _ := aMeshWithSecrets(t) - f, err := gatherFacts(ctx, open, "2.11.17") - if err != nil { - t.Fatal(err) - } - return f, "gathered from a mesh of the test's own" -} - -// 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_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_credentials_integration_test.go b/cmd/mesh-controller/compose_input_credentials_integration_test.go index c0d43de9..c9bcd5d9 100644 --- a/cmd/mesh-controller/compose_input_credentials_integration_test.go +++ b/cmd/mesh-controller/compose_input_credentials_integration_test.go @@ -83,7 +83,7 @@ func TestAPairCredentialNoRowHoldsIsAWouldMakeAndNoRowIsInserted(t *testing.T) { t.Fatal(err) } // The controller's own planning makes the credentials of files and reports. - if _, _, err := planFor(ctx, open, "laptop"); err != nil { + if _, _, err := resolveNode(ctx, open, "laptop"); err != nil { t.Fatal(err) } register(t, open, aConsumerOfTheBucket("metrics")) diff --git a/cmd/mesh-controller/compose_input_integration_test.go b/cmd/mesh-controller/compose_input_integration_test.go index 6fea7665..6e4f190c 100644 --- a/cmd/mesh-controller/compose_input_integration_test.go +++ b/cmd/mesh-controller/compose_input_integration_test.go @@ -280,7 +280,7 @@ func TestAServedFactThatSaysASettingIsResolvedWithTheSettingInTheInput(t *testin 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 := planFor(ctx, open, "laptop") + oldPlan, _, err := resolveNode(ctx, open, "laptop") if err != nil { t.Fatal(err) } @@ -317,7 +317,7 @@ func TestAServedFactThatSaysAnUnsetSettingIsRefusedAsTheControllerRefusesIt(t *t if err := open.inventory.ClearSettings(ctx, "", "step-ca"); err != nil { t.Fatal(err) } - _, _, oldErr := planFor(ctx, open, "laptop") + _, _, 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) 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..2af13fa4 --- /dev/null +++ b/cmd/mesh-controller/compose_made_integration_test.go @@ -0,0 +1,46 @@ +//go:build integration + +package main + +import ( + "context" + "fmt" + "sort" +) + +// 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_perform.go b/cmd/mesh-controller/compose_perform.go index 3305106c..930d6634 100644 --- a/cmd/mesh-controller/compose_perform.go +++ b/cmd/mesh-controller/compose_perform.go @@ -46,26 +46,8 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co for _, line := range d.Passes.Said { fmt.Fprintln(w, line) } - sp := seatPassingOn(inv, w) - for _, p := range d.Passes.Passes { - moved, err := inv.PassSeat(ctx, p.Seat.Name, p.Seat.Scope, p.From, p.To) - if err != nil { - // Said, and the record stays as it was: a composition is never refused for it. - fmt.Fprintf(w, "%s could not be passed from %s on %s to %s on %s: %v\n", p.Seat.Name, - p.From.Module, p.From.Node, p.To.Module, p.To.Node, err) - continue - } - if !moved { - continue // another composition passed it first, and wrote the act - } + if performSeatPasses(ctx, seatPassingOn(inv, w), d.Passes.Passes) { did = true - if inv.Throwaway() { - continue - } - if err := sp.act(ctx, p); err != nil { - fmt.Fprintf(w, "%s passed from %s on %s to %s on %s, and the hand act could NOT be recorded: %v\n", - p.Seat.Name, p.From.Module, p.From.Node, p.To.Module, p.To.Node, err) - } } } @@ -260,7 +242,7 @@ func prepareSend(ctx context.Context, open *stores, node string) ([]string, erro 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) - }, Allocating) + }, true) if err != nil { return nil, err } diff --git a/cmd/mesh-controller/compose_perform_integration_test.go b/cmd/mesh-controller/compose_perform_integration_test.go index 1d9ccbeb..d9ace743 100644 --- a/cmd/mesh-controller/compose_perform_integration_test.go +++ b/cmd/mesh-controller/compose_perform_integration_test.go @@ -59,7 +59,7 @@ func TestPerformAllocatesAForeseenPortAndTheNumberIsTheControllers(t *testing.T) 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 := planFor(ctx, open, "laptop") + plan, settings, err := resolveNode(ctx, open, "laptop") if err != nil { t.Fatal(err) } diff --git a/cmd/mesh-controller/compose_sdk.go b/cmd/mesh-controller/compose_sdk.go index 280fdcdf..56653cf5 100644 --- a/cmd/mesh-controller/compose_sdk.go +++ b/cmd/mesh-controller/compose_sdk.go @@ -119,6 +119,24 @@ func generatorsOf(ctx context.Context, open *stores) (map[string]catalogue.Gener 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). diff --git a/cmd/mesh-controller/compose_sdk_test.go b/cmd/mesh-controller/compose_sdk_test.go index dddfd8a7..8d6a3cb8 100644 --- a/cmd/mesh-controller/compose_sdk_test.go +++ b/cmd/mesh-controller/compose_sdk_test.go @@ -8,7 +8,6 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/compose" - "github.com/novox/mesh-controller/internal/inventory" ) // A declaration as the SDK composes it is the one the controller sends, byte for byte, whatever it carries: the body is the @@ -38,39 +37,28 @@ func TestASendableIsTheComposedDeclarationByteForByte(t *testing.T) { } } -// 1386: what a send records as carried is what the controller's own carriedOn recorded. +// 1386: what a send records as carried: the current build of each module it carries, the build a module left out of it was last +// sent (when that is known), and a module kept at the build the machine runs at that one. (The controller's carriedOn recorded +// the same; mesh-sdk's node_test TestAModuleLeftOutKeepsTheBuildItWasLastSent holds the left-out case.) func TestWhatASendRecordsAsCarriedIsWhatTheControllerRecorded(t *testing.T) { plan := catalogue.Resolution{Modules: []catalogue.Manifest{{Module: "postgres"}, {Module: "files"}, {Module: "cache"}}} - current := map[string]inventory.CurrentBuild{"postgres": {Commit: "p2"}, "files": {Commit: "f2"}, "cache": {Commit: "c2"}} leftOut := map[string]string{"cache": "a setting cannot compose"} for name, c := range map[string]struct { sent map[string]string known bool kept map[string]string + want map[string]string }{ - "nothing sent": {}, - "sent and kept": {sent: map[string]string{"postgres": "p1", "cache": "c1", "files": "f1"}, known: true, kept: map[string]string{"postgres": "p1"}}, - "sent but not known": {sent: map[string]string{"cache": "c1"}}, - "kept a module not in": {known: true, kept: map[string]string{"mail": "m1"}}, + "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"}}, } { - var names []string - for _, m := range plan.Modules { - names = append(names, m.Module) - } - before := c.sent - if !c.known { - before = nil - } - want := carriedBuilds(names, leftOut, current, before) - for m, was := range c.kept { - if _, carried := want[m]; carried { - want[m] = was - } - } in := compose.Input{CurrentBuilds: map[string]string{"postgres": "p2", "files": "f2", "cache": "c2"}} node := compose.Node{SentBuilds: c.sent, SentKnown: c.known, KeptBuilds: c.kept} - if got := buildsOf(in, node, plan, leftOut); !reflect.DeepEqual(got, want) { - t.Errorf("%s: a send records %v, the controller recorded %v", name, got, want) + 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) } } } diff --git a/cmd/mesh-controller/config_moved_test.go b/cmd/mesh-controller/config_moved_test.go index 2692a44e..7aa2aa8c 100644 --- a/cmd/mesh-controller/config_moved_test.go +++ b/cmd/mesh-controller/config_moved_test.go @@ -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 83166b26..33c99192 100644 --- a/cmd/mesh-controller/core_box.go +++ b/cmd/mesh-controller/core_box.go @@ -16,7 +16,6 @@ import ( "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 de2f210d..d26e0e56 100644 --- a/cmd/mesh-controller/core_box_integration_test.go +++ b/cmd/mesh-controller/core_box_integration_test.go @@ -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 56d8782e..1148d0a1 100644 --- a/cmd/mesh-controller/core_box_test.go +++ b/cmd/mesh-controller/core_box_test.go @@ -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 b6f557c4..0cbc1262 100644 --- a/cmd/mesh-controller/credential_on_assign_test.go +++ b/cmd/mesh-controller/credential_on_assign_test.go @@ -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/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 f0e5c41c..67bd4c07 100644 --- a/cmd/mesh-controller/facts.go +++ b/cmd/mesh-controller/facts.go @@ -14,8 +14,6 @@ import ( "sync" "time" - "git.novox.be/novox/mesh-sdk/go/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" @@ -504,41 +502,6 @@ func declarationFacts(ctx context.Context, open *stores, node string, gensErr er 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_test.go b/cmd/mesh-controller/facts_test.go index c3ce5e34..5edc0095 100644 --- a/cmd/mesh-controller/facts_test.go +++ b/cmd/mesh-controller/facts_test.go @@ -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) } } diff --git a/cmd/mesh-controller/generation.go b/cmd/mesh-controller/generation.go index a354d289..ce649ce0 100644 --- a/cmd/mesh-controller/generation.go +++ b/cmd/mesh-controller/generation.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "os" - "sort" "strings" "time" @@ -103,18 +102,6 @@ func senderOf(caller string) string { return fmt.Sprintf("%s (pid %d on %s, build %s)", caller, os.Getpid(), host, version) } -// 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 96362c4b..100d3bb4 100644 --- a/cmd/mesh-controller/grant_follows_binding_integration_test.go +++ b/cmd/mesh-controller/grant_follows_binding_integration_test.go @@ -35,7 +35,7 @@ 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) } @@ -45,7 +45,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) { 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,7 +57,7 @@ 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) } @@ -76,7 +76,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 +87,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 +105,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 +131,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 := resolveNode(ctx, open, "laptop"); err != nil { t.Fatal(err) } @@ -144,10 +144,10 @@ 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) } diff --git a/cmd/mesh-controller/grants_step_store_test.go b/cmd/mesh-controller/grants_step_store_test.go index 206303ab..8432483d 100644 --- a/cmd/mesh-controller/grants_step_store_test.go +++ b/cmd/mesh-controller/grants_step_store_test.go @@ -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/held_back.go b/cmd/mesh-controller/held_back.go index e02c8d34..16ffc4d5 100644 --- a/cmd/mesh-controller/held_back.go +++ b/cmd/mesh-controller/held_back.go @@ -7,7 +7,6 @@ import ( "sort" "strings" - "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) @@ -265,11 +264,9 @@ func flushBehind(ctx context.Context, open *stores, nodes []inventory.Node, hand // composeForPush is how a push composes one machine: its set resolved, what it cannot host and what // 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) { - // Read afresh for each machine: the send before it made and recorded things. gens is the private network the - // controller's own composition took; the SDK's is computed from the same Input. - _ = gens + // 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 { 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 66b66e6d..05e7c65a 100644 --- a/cmd/mesh-controller/held_back_test.go +++ b/cmd/mesh-controller/held_back_test.go @@ -3,7 +3,6 @@ package main import ( "context" "encoding/json" - "reflect" "strings" "testing" "time" @@ -99,22 +98,6 @@ func TestAPlanWaitingOnItsFirstMachineHoldsTheRest(t *testing.T) { } } -// A send records the build of each module it carried; a module left out of it keeps the build it -// was last sent, since the machine keeps that one. -func TestASendCarriesTheCurrentBuildsAndALeftOutModuleKeepsItsOwn(t *testing.T) { - current := map[string]inventory.CurrentBuild{"a": {Commit: "a2"}, "b": {Commit: "b2"}, "c": {}} - got := carriedBuilds([]string{"a", "b", "c"}, map[string]string{"b": "a setting does not compose"}, - current, map[string]string{"a": "a1", "b": "b1"}) - if want := map[string]string{"a": "a2", "b": "b1", "c": ""}; !reflect.DeepEqual(got, want) { - t.Fatalf("carried %v, wanted %v", got, want) - } - // Not known before: the left-out module is not recorded at all, so it reads as never sent. - got = carriedBuilds([]string{"a", "b"}, map[string]string{"b": "x"}, current, nil) - if want := map[string]string{"a": "a2"}; !reflect.DeepEqual(got, want) { - t.Fatalf("carried %v, wanted %v", got, want) - } -} - // recordedDelivery sends nothing and records each send as the mesh does, so the next comparison // reads the machine as current — and writes down which machines it declared. type recordedDelivery struct { diff --git a/cmd/mesh-controller/holdings_test.go b/cmd/mesh-controller/holdings_test.go index 8b3a0054..420360c4 100644 --- a/cmd/mesh-controller/holdings_test.go +++ b/cmd/mesh-controller/holdings_test.go @@ -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 97d5a7a8..01774c33 100644 --- a/cmd/mesh-controller/identity_bound_integration_test.go +++ b/cmd/mesh-controller/identity_bound_integration_test.go @@ -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 1ee6d8eb..a138642c 100644 --- a/cmd/mesh-controller/issue201_integration_test.go +++ b/cmd/mesh-controller/issue201_integration_test.go @@ -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/issued_test.go b/cmd/mesh-controller/issued_test.go index 7752244e..baa1b90a 100644 --- a/cmd/mesh-controller/issued_test.go +++ b/cmd/mesh-controller/issued_test.go @@ -6,6 +6,8 @@ import ( "testing" "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/local_network.go b/cmd/mesh-controller/local_network.go index 833e3af3..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" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/overlay" "github.com/novox/mesh-controller/internal/inventory" ) -// 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/merge_gate_handing_integration_test.go b/cmd/mesh-controller/merge_gate_handing_integration_test.go index 05710b27..58259195 100644 --- a/cmd/mesh-controller/merge_gate_handing_integration_test.go +++ b/cmd/mesh-controller/merge_gate_handing_integration_test.go @@ -8,6 +8,7 @@ import ( "time" "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 318c73ff..73c5a9da 100644 --- a/cmd/mesh-controller/merge_gate_handing_test.go +++ b/cmd/mesh-controller/merge_gate_handing_test.go @@ -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/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_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 50b4c961..7dd25721 100644 --- a/cmd/mesh-controller/modules_wire_test.go +++ b/cmd/mesh-controller/modules_wire_test.go @@ -6,7 +6,6 @@ import ( "testing" "git.novox.be/novox/mesh-sdk/go/validate" - "github.com/novox/mesh-controller/internal/chain" ) // 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/network.go b/cmd/mesh-controller/network.go index 2137473e..a05ee537 100644 --- a/cmd/mesh-controller/network.go +++ b/cmd/mesh-controller/network.go @@ -316,73 +316,6 @@ func graph(ctx context.Context, open *stores) ([]overlay.Node, overlay.Graph, er return compose.Graph(in.Input) } -// 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 -} - func overlayShow(ctx context.Context, open *stores) error { nodes, computed, err := graph(ctx, open) if err != nil { @@ -576,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 9ff9056d..92ae3d31 100644 --- a/cmd/mesh-controller/network_test.go +++ b/cmd/mesh-controller/network_test.go @@ -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/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/outside_accept_test.go b/cmd/mesh-controller/outside_accept_test.go index e3aa7cd9..f34f8d58 100644 --- a/cmd/mesh-controller/outside_accept_test.go +++ b/cmd/mesh-controller/outside_accept_test.go @@ -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 e1becf05..c38ae56e 100644 --- a/cmd/mesh-controller/outside_not_given_test.go +++ b/cmd/mesh-controller/outside_not_given_test.go @@ -6,6 +6,7 @@ import ( "testing" "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/placement_moved_test.go b/cmd/mesh-controller/placement_moved_test.go index b873070c..661f0691 100644 --- a/cmd/mesh-controller/placement_moved_test.go +++ b/cmd/mesh-controller/placement_moved_test.go @@ -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_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 40e34796..bd0a70fe 100644 --- a/cmd/mesh-controller/plan.go +++ b/cmd/mesh-controller/plan.go @@ -15,11 +15,8 @@ import ( "sync" "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/chain" "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/licences" "github.com/novox/mesh-controller/internal/link" ) @@ -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