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 198be8217c, built from fb556d6b by patch commits and compared with the author's worktree), so that this commit's parent is fb556d6b.
This commit is contained in:
2026-10-20 11:18:42 +00:00
parent fb556d6b74
commit 2111b78fc5
67 changed files with 378 additions and 2605 deletions
+1 -10
View File
@@ -13,7 +13,6 @@ import (
"time" "time"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose"
"git.novox.be/novox/mesh-sdk/go/overlay" "git.novox.be/novox/mesh-sdk/go/overlay"
"github.com/novox/mesh-controller/internal/inventory" "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. // 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 { if err != nil {
return "", err 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) rules, err := plan.Rules(with)
if err != nil { if err != nil {
return "", err return "", err
@@ -9,6 +9,7 @@ import (
"time" "time"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose"
"git.novox.be/novox/mesh-sdk/go/overlay" "git.novox.be/novox/mesh-sdk/go/overlay"
"github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
@@ -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 // 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. // can be compared about a secret that does not exist (status), and nothing here string-matches.
plan, settings, err := planFor(ctx, open, "laptop") _, _, err := composeQuestion(ctx, open, "laptop", compose.Reading, false)
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)
var notMade *catalogue.NotMadeError var notMade *catalogue.NotMadeError
if !errors.As(err, &notMade) || notMade.Module != "keeper" || notMade.Name != "repository" { if !errors.As(err, &notMade) || notMade.Module != "keeper" || notMade.Name != "repository" {
t.Fatalf("a read composition did not say which secret is not made, typed: %v", err) 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, 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" { if err != nil || len(foreseen.foreseen) != 1 || foreseen.foreseen[0] != "keeper/repository" {
t.Fatalf("foreseen: %v %v", foreseen.foreseen, err) t.Fatalf("foreseen: %v %v", foreseen.foreseen, err)
} }
+3 -4
View File
@@ -29,7 +29,7 @@ func aKeeper() catalogue.Manifest {
func pushedBy(t *testing.T, open *stores, node string) string { func pushedBy(t *testing.T, open *stores, node string) string {
t.Helper() t.Helper()
ctx := t.Context() ctx := t.Context()
plan, settings, err := planFor(ctx, open, node) plan, settings, err := resolveNode(ctx, open, node)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -54,11 +54,10 @@ func pushedBy(t *testing.T, open *stores, node string) string {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
names := make([]string, 0, len(plan.Modules)) builds = map[string]string{}
for _, m := range plan.Modules { 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 { if err := open.inventory.RecordSent(ctx, record.ID, digest, builds); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -37,7 +37,7 @@ func TestTheIncidentAConsumerStaysBesideItsDataUntilAPersonPinsIt(t *testing.T)
assignAll([2]string{"laptop", "store"}, [2]string{"laptop", "resolver"}, [2]string{"laptop", "network"}, assignAll([2]string{"laptop", "store"}, [2]string{"laptop", "resolver"}, [2]string{"laptop", "network"},
[2]string{"laptop", "board"}) [2]string{"laptop", "board"})
plan, _, err := planFor(ctx, open, "laptop") plan, _, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -64,7 +64,7 @@ func TestTheIncidentAConsumerStaysBesideItsDataUntilAPersonPinsIt(t *testing.T)
if err := inv.Unassign(ctx, "laptop", "store"); err != nil { if err := inv.Unassign(ctx, "laptop", "store"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if _, _, err := planFor(ctx, open, "laptop"); err == nil || !unresolvable(err) || if _, _, err := resolveNode(ctx, open, "laptop"); err == nil || !unresolvable(err) ||
!strings.Contains(err.Error(), "board on laptop is bound to laptop/store") || !strings.Contains(err.Error(), "board on laptop is bound to laptop/store") ||
!strings.Contains(err.Error(), "pin laptop postgres-database anchor 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) 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 { if err := inv.PinProvision(ctx, "laptop", "postgres-database", "anchor", "store"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
plan, _, err = planFor(ctx, open, "laptop") plan, _, err = resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+17 -21
View File
@@ -10,21 +10,13 @@ import (
"testing" "testing"
) )
// 1388: no caller composes through the controller's own composition any more. The only functions left that call it are the // 1389: the controller's own composition is gone. No function of the package is named for it any more, and no function
// composition's own, which issue 1389 deletes with it; this fails the moment any other function reaches for it again. // 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 TestNoCallerComposesThroughTheControllersOwnComposition(t *testing.T) { func TestTheControllersOwnCompositionIsGone(t *testing.T) {
legacy := map[string]bool{"planFor": true, "declarationWith": true, "composedAndValidated": true, "generators": true, gone := map[string]bool{"planFor": true, "declarationWith": true, "composedAndValidated": true, "generators": true,
"renderingFor": true, "theRestOfTheMesh": true, "whoResolves": true} "renderingFor": true, "theRestOfTheMesh": true, "whoResolves": true, "grantsFor": true, "namesInTheMesh": true,
// The composition's own pairs, until 1389: "file: caller calls callee". "onThePrivateNetwork": true, "passSeatsOn": true, "theMeshPassesSeatsOn": true, "assignmentsOf": true,
allowed := map[string]bool{ "licencesFor": true, "seatsOn": true, "composeBusUsers": true, "adoptionFor": true, "localNetworksOf": true}
"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,
}
files, err := filepath.Glob("*.go") files, err := filepath.Glob("*.go")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -41,12 +33,18 @@ func TestNoCallerComposesThroughTheControllersOwnComposition(t *testing.T) {
} }
for _, d := range file.Decls { for _, d := range file.Decls {
fn, ok := d.(*ast.FuncDecl) 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 continue
} }
ast.Inspect(fn.Body, func(n ast.Node) bool { ast.Inspect(fn.Body, func(n ast.Node) bool {
if c, ok := n.(*ast.CallExpr); ok { 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) found = append(found, f+": "+fn.Name.Name+" calls "+id.Name)
} }
} }
@@ -55,9 +53,7 @@ func TestNoCallerComposesThroughTheControllersOwnComposition(t *testing.T) {
} }
} }
sort.Strings(found) sort.Strings(found)
for _, pair := range found { for _, one := range found {
if !allowed[pair] { t.Errorf("%s: the controller's own composition is back; compose through mesh-sdk (compose_sdk.go)", one)
t.Errorf("%s: it composes through the controller's own composition, not mesh-sdk's", pair)
}
} }
} }
@@ -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
}
@@ -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)
}
@@ -83,7 +83,7 @@ func TestAPairCredentialNoRowHoldsIsAWouldMakeAndNoRowIsInserted(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// The controller's own planning makes the credentials of files and reports. // 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) t.Fatal(err)
} }
register(t, open, aConsumerOfTheBucket("metrics")) register(t, open, aConsumerOfTheBucket("metrics"))
@@ -280,7 +280,7 @@ func TestAServedFactThatSaysASettingIsResolvedWithTheSettingInTheInput(t *testin
if anchor, _ := nodeOf(in.Input, "anchor"); len(anchor.Settings["step-ca"]) == 0 { 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) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -317,7 +317,7 @@ func TestAServedFactThatSaysAnUnsetSettingIsRefusedAsTheControllerRefusesIt(t *t
if err := open.inventory.ClearSettings(ctx, "", "step-ca"); err != nil { if err := open.inventory.ClearSettings(ctx, "", "step-ca"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
_, _, oldErr := planFor(ctx, open, "laptop") _, _, oldErr := resolveNode(ctx, open, "laptop")
_, newErr := readComposeInput(ctx, open) _, newErr := readComposeInput(ctx, open)
if (oldErr == nil) != (newErr == nil) { if (oldErr == nil) != (newErr == nil) {
t.Fatalf("the controller's composition says %v, the adapter says %v", oldErr, newErr) t.Fatalf("the controller's composition says %v, the adapter says %v", oldErr, newErr)
@@ -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
}
+2 -20
View File
@@ -46,26 +46,8 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co
for _, line := range d.Passes.Said { for _, line := range d.Passes.Said {
fmt.Fprintln(w, line) fmt.Fprintln(w, line)
} }
sp := seatPassingOn(inv, w) if performSeatPasses(ctx, seatPassingOn(inv, w), d.Passes.Passes) {
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
}
did = true 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 { if n.Stated && n.Contract >= compose.VaultBoxContract {
boxes, err := coreBoxesFor(ctx, inv, node, resolved.Plan.Modules, func(m catalogue.Manifest) bool { boxes, err := coreBoxesFor(ctx, inv, node, resolved.Plan.Modules, func(m catalogue.Manifest) bool {
return handing.Handing(m) return handing.Handing(m)
}, Allocating) }, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -59,7 +59,7 @@ func TestPerformAllocatesAForeseenPortAndTheNumberIsTheControllers(t *testing.T)
t.Fatalf("a second composition names %v foreseen: %v", again.ForeseenPorts, err) t.Fatalf("a second composition names %v foreseen: %v", again.ForeseenPorts, err)
} }
// The number is the one the controller allocates in the same store. // 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+18
View File
@@ -119,6 +119,24 @@ func generatorsOf(ctx context.Context, open *stores) (map[string]catalogue.Gener
return compose.Generators(in.Input) 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 // 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 // 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). // could not be asked about is an error, never a refusal (novox/hq 04-ISSUES/152).
+11 -23
View File
@@ -8,7 +8,6 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose" "git.novox.be/novox/mesh-sdk/go/compose"
"github.com/novox/mesh-controller/internal/inventory"
) )
// A declaration as the SDK composes it is the one the controller sends, byte for byte, whatever it carries: the body is the // A declaration as the SDK composes it is the one the controller sends, byte for byte, whatever it carries: the body is the
@@ -38,39 +37,28 @@ func TestASendableIsTheComposedDeclarationByteForByte(t *testing.T) {
} }
} }
// 1386: what a send records as carried is what the controller's own carriedOn recorded. // 1386: what a send records as carried: the current build of each module it carries, the build a module left out of it was last
// sent (when that is known), and a module kept at the build the machine runs at that one. (The controller's carriedOn recorded
// the same; mesh-sdk's node_test TestAModuleLeftOutKeepsTheBuildItWasLastSent holds the left-out case.)
func TestWhatASendRecordsAsCarriedIsWhatTheControllerRecorded(t *testing.T) { func TestWhatASendRecordsAsCarriedIsWhatTheControllerRecorded(t *testing.T) {
plan := catalogue.Resolution{Modules: []catalogue.Manifest{{Module: "postgres"}, {Module: "files"}, {Module: "cache"}}} plan := catalogue.Resolution{Modules: []catalogue.Manifest{{Module: "postgres"}, {Module: "files"}, {Module: "cache"}}}
current := map[string]inventory.CurrentBuild{"postgres": {Commit: "p2"}, "files": {Commit: "f2"}, "cache": {Commit: "c2"}}
leftOut := map[string]string{"cache": "a setting cannot compose"} leftOut := map[string]string{"cache": "a setting cannot compose"}
for name, c := range map[string]struct { for name, c := range map[string]struct {
sent map[string]string sent map[string]string
known bool known bool
kept map[string]string kept map[string]string
want map[string]string
}{ }{
"nothing sent": {}, "nothing sent": {want: map[string]string{"postgres": "p2", "files": "f2"}},
"sent and kept": {sent: map[string]string{"postgres": "p1", "cache": "c1", "files": "f1"}, known: true, kept: map[string]string{"postgres": "p1"}}, "sent and kept": {sent: map[string]string{"postgres": "p1", "cache": "c1", "files": "f1"}, known: true,
"sent but not known": {sent: map[string]string{"cache": "c1"}}, kept: map[string]string{"postgres": "p1"}, want: map[string]string{"postgres": "p1", "files": "f2", "cache": "c1"}},
"kept a module not in": {known: true, kept: map[string]string{"mail": "m1"}}, "sent but not known": {sent: map[string]string{"cache": "c1"}, want: map[string]string{"postgres": "p2", "files": "f2"}},
"kept a module not in": {known: true, kept: map[string]string{"mail": "m1"}, want: map[string]string{"postgres": "p2", "files": "f2"}},
} { } {
var names []string
for _, m := range plan.Modules {
names = append(names, m.Module)
}
before := c.sent
if !c.known {
before = nil
}
want := carriedBuilds(names, leftOut, current, before)
for m, was := range c.kept {
if _, carried := want[m]; carried {
want[m] = was
}
}
in := compose.Input{CurrentBuilds: map[string]string{"postgres": "p2", "files": "f2", "cache": "c2"}} in := compose.Input{CurrentBuilds: map[string]string{"postgres": "p2", "files": "f2", "cache": "c2"}}
node := compose.Node{SentBuilds: c.sent, SentKnown: c.known, KeptBuilds: c.kept} node := compose.Node{SentBuilds: c.sent, SentKnown: c.known, KeptBuilds: c.kept}
if got := buildsOf(in, node, plan, leftOut); !reflect.DeepEqual(got, want) { if got := buildsOf(in, node, plan, leftOut); !reflect.DeepEqual(got, c.want) {
t.Errorf("%s: a send records %v, the controller recorded %v", name, got, want) t.Errorf("%s: a send records %v, want %v", name, got, c.want)
} }
} }
} }
+2 -6
View File
@@ -127,21 +127,17 @@ type snapshotFacts = snapshot.Facts
func toldEveryMachine(t *testing.T, open *stores, served map[string]map[string]any, on map[string][]string) map[string]string { func toldEveryMachine(t *testing.T, open *stores, served map[string]map[string]any, on map[string][]string) map[string]string {
t.Helper() t.Helper()
ctx := t.Context() ctx := t.Context()
gens, err := generators(ctx, open)
if err != nil {
t.Fatal(err)
}
shelf, err := open.inventory.Catalogue(ctx) shelf, err := open.inventory.Catalogue(ctx)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
told := map[string]string{} told := map[string]string{}
for _, node := range fourMachines { for _, node := range fourMachines {
plan, settings, err := planFor(ctx, open, node) plan, _, err := resolveNode(ctx, open, node)
if err != nil { if err != nil {
t.Fatalf("%s: %v", node, err) t.Fatalf("%s: %v", node, err)
} }
with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Allocating) with, err := madeAndRendered(ctx, open, node)
if err != nil { if err != nil {
t.Fatalf("%s: %v", node, err) t.Fatalf("%s: %v", node, err)
} }
+7 -17
View File
@@ -16,7 +16,6 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
) )
// The core's own credentials, sealed by the vault (novox/hq ADR 0349 §16, issue 755; the lead's decision on issue 755, // The core's own credentials, sealed by the vault (novox/hq ADR 0349 §16, issue 755; the lead's decision on issue 755,
@@ -40,15 +39,6 @@ import (
// the vault and the machine, cleared by the first send the vault answers whole. Only a machine with no sealing key, or // the vault and the machine, cleared by the first send the vault answers whole. Only a machine with no sealing key, or
// one whose key cannot be read, leaves its module out. // one whose key cannot be read, leaves its module out.
// engineOpensVaultBox says whether a machine's node-engine opens the vault's box, by its own newest statement.
func engineOpensVaultBox(ctx context.Context, inv *inventory.Inventory, node string) (bool, error) {
stated, had, err := inv.HealthOf(ctx, node)
if err != nil {
return false, err
}
return had && stated.Contract >= link.VaultBoxContract, nil
}
// coreAnswer is the vault's `get` answer as the controller reads it: the name, the generation and the box, never // coreAnswer is the vault's `get` answer as the controller reads it: the name, the generation and the box, never
// opened. // opened.
type coreAnswer struct { type coreAnswer struct {
@@ -121,7 +111,7 @@ func sealingKeyPEM(raw string) (string, error) {
// own seal where nothing was kept; on any other question, the kept box, or a stand-in the next send asks for. Pure: what to keep and what to // own seal where nothing was kept; on any other question, the kept box, or a stand-in the next send asks for. Pure: what to keep and what to
// say is answered, not done. // say is answered, not done.
func coreBoxesOf(node, sealingKey string, core map[string][]string, kept map[string]map[string]inventory.CoreBox, func coreBoxesOf(node, sealingKey string, core map[string][]string, kept map[string]map[string]inventory.CoreBox,
choosing Choosing, ask coreAsk) coreBoxes { allocating bool, ask coreAsk) coreBoxes {
out := coreBoxes{sealed: map[string]map[string]json.RawMessage{}, notPlaced: map[string]string{}} out := coreBoxes{sealed: map[string]map[string]json.RawMessage{}, notPlaced: map[string]string{}}
modules := make([]string, 0, len(core)) modules := make([]string, 0, len(core))
for m := range core { for m := range core {
@@ -143,7 +133,7 @@ func coreBoxesOf(node, sealingKey string, core map[string][]string, kept map[str
for _, name := range core[module] { for _, name := range core[module] {
k, has := kept[module][name] k, has := kept[module][name]
valid := has && k.SealedTo == sealingKey && len(k.Box) > 0 valid := has && k.SealedTo == sealingKey && len(k.Box) > 0
if choosing != Allocating { if !allocating {
if valid { if valid {
boxes[name] = k.Box boxes[name] = k.Box
} else { } else {
@@ -218,10 +208,10 @@ var coreVault = func(ctx context.Context) coreAsk {
// coreBoxesFor is coreBoxesOf for one machine, read from and kept in the store, and said on a send. // coreBoxesFor is coreBoxesOf for one machine, read from and kept in the store, and said on a send.
func coreBoxesFor(ctx context.Context, inv *inventory.Inventory, node string, modules []catalogue.Manifest, func coreBoxesFor(ctx context.Context, inv *inventory.Inventory, node string, modules []catalogue.Manifest,
handed func(catalogue.Manifest) bool, choosing Choosing) (coreBoxes, error) { handed func(catalogue.Manifest) bool, allocating bool) (coreBoxes, error) {
core := coreModulesOf(modules, handed) core := coreModulesOf(modules, handed)
if len(core) == 0 { if len(core) == 0 {
if choosing == Allocating { if allocating {
settleVaultSilent(ctx, node) settleVaultSilent(ctx, node)
} }
return coreBoxes{}, nil return coreBoxes{}, nil
@@ -235,16 +225,16 @@ func coreBoxesFor(ctx context.Context, inv *inventory.Inventory, node string, mo
return coreBoxes{}, err return coreBoxes{}, err
} }
var ask coreAsk var ask coreAsk
if choosing == Allocating { if allocating {
ask = coreVault(ctx) ask = coreVault(ctx)
} }
out := coreBoxesOf(node, key, core, kept, choosing, ask) out := coreBoxesOf(node, key, core, kept, allocating, ask)
for _, b := range out.keep { for _, b := range out.keep {
if err := inv.KeepCoreBox(ctx, node, b); err != nil { if err := inv.KeepCoreBox(ctx, node, b); err != nil {
return coreBoxes{}, err return coreBoxes{}, err
} }
} }
if choosing == Allocating { if allocating {
sayVaultSilent(ctx, node, out) sayVaultSilent(ctx, node, out)
} }
return out, nil return out, nil
@@ -96,7 +96,7 @@ func TestASilentVaultHoldsTheKeptBoxAndSaysSo(t *testing.T) {
func TestACoreModuleFirstPlacedWhileTheVaultIsSilentIsSealedByTheController(t *testing.T) { func TestACoreModuleFirstPlacedWhileTheVaultIsSilentIsSealedByTheController(t *testing.T) {
open, vault := aCoreMesh(t, link.VaultBoxContract) open, vault := aCoreMesh(t, link.VaultBoxContract)
vault.silent = errors.New("no responders") vault.silent = errors.New("no responders")
plan, settings, err := planFor(t.Context(), open, "laptop") plan, settings, err := resolveNode(t.Context(), open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+6 -6
View File
@@ -68,7 +68,7 @@ func TestReplay908(t *testing.T) {
key := "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" key := "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
core := map[string][]string{"nats": {"password"}} core := map[string][]string{"nats": {"password"}}
ask := func(name, publicKey string) (coreAnswer, error) { return coreAnswer{}, errors.New("no responders") } ask := func(name, publicKey string) (coreAnswer, error) { return coreAnswer{}, errors.New("no responders") }
got := coreBoxesOf("novox", key, core, nil, Allocating, ask) got := coreBoxesOf("novox", key, core, nil, true, ask)
if _, sealed := got.sealed["nats"]; sealed || len(got.notPlaced) != 0 { if _, sealed := got.sealed["nats"]; sealed || len(got.notPlaced) != 0 {
t.Fatalf("nats is vault-sealed %v or not placed %v", got.sealed, got.notPlaced) t.Fatalf("nats is vault-sealed %v or not placed %v", got.sealed, got.notPlaced)
} }
@@ -81,7 +81,7 @@ func TestReplay908(t *testing.T) {
} }
// A kept box is still carried while the vault is silent. // A kept box is still carried while the vault is silent.
kept := map[string]map[string]inventory.CoreBox{"nats": {"password": {SealedTo: key, Box: json.RawMessage(`{"v":1}`)}}} kept := map[string]map[string]inventory.CoreBox{"nats": {"password": {SealedTo: key, Box: json.RawMessage(`{"v":1}`)}}}
got = coreBoxesOf("novox", key, core, kept, Allocating, ask) got = coreBoxesOf("novox", key, core, kept, true, ask)
if string(got.sealed["nats"]["password"]) != `{"v":1}` || len(got.byController) != 0 { if string(got.sealed["nats"]["password"]) != `{"v":1}` || len(got.byController) != 0 {
t.Fatalf("the kept box was not held: %+v", got) t.Fatalf("the kept box was not held: %+v", got)
} }
@@ -99,17 +99,17 @@ func TestOnlyASendAsksTheVault(t *testing.T) {
t.Fatal("the vault was asked by a question that is not a send") t.Fatal("the vault was asked by a question that is not a send")
return coreAnswer{}, nil return coreAnswer{}, nil
} }
for _, choosing := range []Choosing{Reading, Foreseeing} { for _, allocating := range []bool{false} {
if got := coreBoxesOf("anchor", key, core, kept, choosing, never); string(got.sealed["nats"]["broker"]) != `{"v":1}` { if got := coreBoxesOf("anchor", key, core, kept, allocating, never); string(got.sealed["nats"]["broker"]) != `{"v":1}` {
t.Errorf("read %s", got.sealed["nats"]["broker"]) t.Errorf("read %s", got.sealed["nats"]["broker"])
} }
if got := coreBoxesOf("anchor", key, core, nil, choosing, never); string(got.sealed["nats"]["broker"]) != if got := coreBoxesOf("anchor", key, core, nil, allocating, never); string(got.sealed["nats"]["broker"]) !=
string(catalogue.ForeseenVaultBox) { string(catalogue.ForeseenVaultBox) {
t.Errorf("with none kept, read %s", got.sealed["nats"]["broker"]) t.Errorf("with none kept, read %s", got.sealed["nats"]["broker"])
} }
} }
// A box kept for another key of the machine is never carried. // A box kept for another key of the machine is never carried.
other := coreBoxesOf("anchor", aPublicKey(t), core, kept, Allocating, other := coreBoxesOf("anchor", aPublicKey(t), core, kept, true,
func(string, string) (coreAnswer, error) { return coreAnswer{}, errors.New("silent") }) func(string, string) (coreAnswer, error) { return coreAnswer{}, errors.New("silent") })
if _, vaultSealed := other.sealed["nats"]; vaultSealed || !reflect.DeepEqual(other.byController, []string{"nats"}) { if _, vaultSealed := other.sealed["nats"]; vaultSealed || !reflect.DeepEqual(other.byController, []string{"nats"}) {
t.Fatalf("a box sealed to the machine's old key was carried: %+v", other) t.Fatalf("a box sealed to the machine's old key was carried: %+v", other)
@@ -40,7 +40,7 @@ func TestAssigningAModuleThatSpeaksOnTheBusNamesItsCredential(t *testing.T) {
} }
// And the push refuses to send it, naming the same verb, rather than sealing a placeholder. // And the push refuses to send it, naming the same verb, rather than sealing a placeholder.
plan, settings, err := planFor(ctx, open, "laptop") plan, settings, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+2 -10
View File
@@ -42,12 +42,8 @@ func TestAMachineIsSentTheEpochOnlyOnceItSaysItReadsOne(t *testing.T) {
if err := inv.RecordReadsEpoch(ctx, anchor.ID, true); err != nil { if err := inv.RecordReadsEpoch(ctx, anchor.ID, true); err != nil {
t.Fatal(err) t.Fatal(err)
} }
gens, err := generators(ctx, open)
if err != nil {
t.Fatal(err)
}
d := &bodiesDelivery{recordedDelivery: recordedDelivery{inv: inv}, bodies: map[string][]byte{}} d := &bodiesDelivery{recordedDelivery: recordedDelivery{inv: inv}, bodies: map[string][]byte{}}
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, composeForPush(open, gens), d, "", nil); err != nil { if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, composeForPush(open), d, "", nil); err != nil {
t.Fatal(err) t.Fatal(err)
} }
carried := func(node string) (epoch float64, has bool) { carried := func(node string) (epoch float64, has bool) {
@@ -89,12 +85,8 @@ func TestNothingIsComposedOrSentWithoutTheLease(t *testing.T) {
was := epochForActs was := epochForActs
epochForActs = func(context.Context) (uint64, error) { return 0, errors.New("this controller lost the lease") } epochForActs = func(context.Context) (uint64, error) { return 0, errors.New("this controller lost the lease") }
t.Cleanup(func() { epochForActs = was }) t.Cleanup(func() { epochForActs = was })
gens, err := generators(ctx, open)
if err != nil {
t.Fatal(err)
}
d := &bodiesDelivery{recordedDelivery: recordedDelivery{inv: open.inventory}, bodies: map[string][]byte{}} d := &bodiesDelivery{recordedDelivery: recordedDelivery{inv: open.inventory}, bodies: map[string][]byte{}}
refused, err := sendRound(ctx, open, []string{"anchor"}, composeForPush(open, gens), d, "", nil) refused, err := sendRound(ctx, open, []string{"anchor"}, composeForPush(open), d, "", nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
-37
View File
@@ -14,8 +14,6 @@ import (
"sync" "sync"
"time" "time"
"git.novox.be/novox/mesh-sdk/go/validate"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose" "git.novox.be/novox/mesh-sdk/go/compose"
snapshot "git.novox.be/novox/mesh-sdk/go/facts" snapshot "git.novox.be/novox/mesh-sdk/go/facts"
@@ -504,41 +502,6 @@ func declarationFacts(ctx context.Context, open *stores, node string, gensErr er
return d return d
} }
// composedAndValidated composes one machine's declaration — as a push would (Allocating) or as the next
// push will without making anything (Foreseeing) — with the order it was last sent, and runs the
// node-engine's own validator over the body. An error is that it did not compose; problems are what the
// validator refuses.
func composedAndValidated(ctx context.Context, open *stores, node string, gens map[string]catalogue.Generator,
choosing Choosing) (sendable, []string, error) {
plan, settings, err := planFor(ctx, open, node)
if err != nil {
return sendable{}, nil, err
}
declared, err := declarationWith(ctx, open, node, plan, settings, gens, choosing)
if err != nil {
return sendable{}, nil, err
}
record, err := open.inventory.NodeByName(ctx, node)
if err != nil {
return sendable{}, nil, err
}
if declared.Sequence, err = open.inventory.Sequence(ctx, record.ID); err != nil {
return sendable{}, nil, err
}
if declared.Epoch, err = open.inventory.SentEpoch(ctx, record.ID); err != nil {
return sendable{}, nil, err
}
// And the generation it was last sent, as the would-send is (novox/hq issue 234).
if declared.Generation, err = open.inventory.SentGeneration(ctx, record.ID); err != nil {
return sendable{}, nil, err
}
body, err := declared.Body()
if err != nil {
return sendable{}, nil, err
}
return declared, validate.Declaration(body), nil
}
// resourceNames are a declaration's resources as `type:id`, sorted. // resourceNames are a declaration's resources as `type:id`, sorted.
func resourceNames(resources []map[string]any) []string { func resourceNames(resources []map[string]any) []string {
out := make([]string, 0, len(resources)) out := make([]string, 0, len(resources))
+1 -5
View File
@@ -42,12 +42,8 @@ func aMeshWithSecrets(t *testing.T) (*stores, []string) {
t.Fatal(err) t.Fatal(err)
} }
// A push's worth of composition, which makes the pair credential the consumer is sent. // A push's worth of composition, which makes the pair credential the consumer is sent.
gens, err := generators(ctx, open)
if err != nil {
t.Fatal(err)
}
for _, node := range []string{"laptop", "anchor"} { for _, node := range []string{"laptop", "anchor"} {
if _, _, err := composedAndValidated(ctx, open, node, gens, Allocating); err != nil { if _, err := composeSend(ctx, open, node); err != nil {
t.Fatalf("%s does not compose: %v", node, err) t.Fatalf("%s does not compose: %v", node, err)
} }
} }
-13
View File
@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"sort"
"strings" "strings"
"time" "time"
@@ -103,18 +102,6 @@ func senderOf(caller string) string {
return fmt.Sprintf("%s (pid %d on %s, build %s)", caller, os.Getpid(), host, version) return fmt.Sprintf("%s (pid %d on %s, build %s)", caller, os.Getpid(), host, version)
} }
// namedModules are the modules a declaration names: its machine's set, less what was left out of it.
func namedModules(plan catalogue.Resolution, leftOut map[string]string) []string {
out := make([]string, 0, len(plan.Modules))
for _, m := range plan.Modules {
if _, left := leftOut[m.Module]; !left {
out = append(out, m.Module)
}
}
sort.Strings(out)
return out
}
// heardGenerationRefusal keeps a machine's refusal of a send for its generation, so S20 names its sender // heardGenerationRefusal keeps a machine's refusal of a send for its generation, so S20 names its sender
// and says it (novox/hq issue 234): on the send it refused or — when the mesh has no record of sending that // and says it (novox/hq issue 234): on the send it refused or — when the mesh has no record of sending that
// sequence — as a refusal of its own, naming the sender as unknown. Either way S20 is raised: a refusal the // sequence — as a refusal of its own, naming the sender as unknown. Either way S20 is raised: a refusal the
@@ -35,7 +35,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
if err := inv.PinProvision(ctx, "laptop", "postgres-database", "anchor", "store"); err != nil { if err := inv.PinProvision(ctx, "laptop", "postgres-database", "anchor", "store"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
plan, _, err := planFor(ctx, open, "laptop") plan, _, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -45,7 +45,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
if err := inv.RecordBindings(ctx, "laptop", boundToData(plan, nil)); err != nil { if err := inv.RecordBindings(ctx, "laptop", boundToData(plan, nil)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
grants, _, unbound, err := grantsFor(ctx, open, "anchor") grants, _, unbound, err := grantsOf(ctx, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -57,7 +57,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
if err := inv.PinProvision(ctx, "laptop", "postgres-database", "laptop", "store"); err != nil { if err := inv.PinProvision(ctx, "laptop", "postgres-database", "laptop", "store"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
plan, _, err = planFor(ctx, open, "laptop") plan, _, err = resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -76,7 +76,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
} }
// The store it left: no longer granted, and said. // The store it left: no longer granted, and said.
grants, _, unbound, err = grantsFor(ctx, open, "anchor") grants, _, unbound, err = grantsOf(ctx, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -87,7 +87,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
strings.Join(unbound[0].BoundTo, ",") != "laptop" { strings.Join(unbound[0].BoundTo, ",") != "laptop" {
t.Fatalf("the consumer that moved is not the one said: %+v", unbound) t.Fatalf("the consumer that moved is not the one said: %+v", unbound)
} }
planned, settings, err := planFor(ctx, open, "anchor") planned, settings, err := resolveNode(ctx, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -105,7 +105,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
} }
// The store it is bound to: granted. // The store it is bound to: granted.
grants, _, unbound, err = grantsFor(ctx, open, "laptop") grants, _, unbound, err = grantsOf(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -131,7 +131,7 @@ func TestAConsumerWhoseResolutionCannotBeReadIsNotWithdrawn(t *testing.T) {
t.Fatalf("assign %s %s: %v", a[0], a[1], err) t.Fatalf("assign %s %s: %v", a[0], a[1], err)
} }
} }
if _, _, err := planFor(ctx, open, "laptop"); err != nil { if _, _, err := resolveNode(ctx, open, "laptop"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -144,10 +144,10 @@ func TestAConsumerWhoseResolutionCannotBeReadIsNotWithdrawn(t *testing.T) {
if err := inv.RecordSealingKey(ctx, laptop.ID, "not a key"); err != nil { if err := inv.RecordSealingKey(ctx, laptop.ID, "not a key"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if _, _, err := planFor(ctx, open, "laptop"); err == nil || unresolvable(err) { if _, _, err := resolveNode(ctx, open, "laptop"); err == nil || unresolvable(err) {
t.Fatalf("the seam this test relies on moved: %v", err) t.Fatalf("the seam this test relies on moved: %v", err)
} }
grants, _, unbound, err := grantsFor(ctx, open, "anchor") grants, _, unbound, err := grantsOf(ctx, open, "anchor")
if err == nil { if err == nil {
t.Fatalf("an unreadable consumer was answered: grants %+v, unbound %+v", grants, unbound) t.Fatalf("an unreadable consumer was answered: grants %+v, unbound %+v", grants, unbound)
} }
@@ -66,15 +66,11 @@ func TestTheGrantsStepSendsTheUserListAndNothingElse(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
plan, settings, err := planFor(under, open, "anchor") plan, _, err := resolveNode(under, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
gens, err := generators(under, open) declared, err := composeSend(under, open, "anchor")
if err != nil {
t.Fatal(err)
}
declared, err := declarationWith(under, open, "anchor", plan, settings, gens, Allocating)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+2 -5
View File
@@ -7,7 +7,6 @@ import (
"sort" "sort"
"strings" "strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
) )
@@ -265,11 +264,9 @@ func flushBehind(ctx context.Context, open *stores, nodes []inventory.Node, hand
// composeForPush is how a push composes one machine: its set resolved, what it cannot host and what // composeForPush is how a push composes one machine: its set resolved, what it cannot host and what
// is left out of it said, and its declaration allocated. // is left out of it said, and its declaration allocated.
func composeForPush(open *stores, gens map[string]catalogue.Generator) func(held context.Context, node string) (sendable, error) { func composeForPush(open *stores) func(held context.Context, node string) (sendable, error) {
return func(held context.Context, node string) (sendable, error) { return func(held context.Context, node string) (sendable, error) {
// Read afresh for each machine: the send before it made and recorded things. gens is the private network the // Read afresh for each machine: the send before it made and recorded things.
// controller's own composition took; the SDK's is computed from the same Input.
_ = gens
held = freshInput(held) held = freshInput(held)
plan, _, err := resolveNode(held, open, node) plan, _, err := resolveNode(held, open, node)
if err != nil { if err != nil {
@@ -28,11 +28,7 @@ func TestANamedPushLeavesAMachineAPolicyHoldsBack(t *testing.T) {
if err := inv.SetUpgradeOf(ctx, "resolver", inventory.Upgrade{Why: "each machine checked by hand"}); err != nil { if err := inv.SetUpgradeOf(ctx, "resolver", inventory.Upgrade{Why: "each machine checked by hand"}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
gens, err := generators(ctx, open) compose := composeForPush(open)
if err != nil {
t.Fatal(err)
}
compose := composeForPush(open, gens)
d := &recordedDelivery{inv: inv} d := &recordedDelivery{inv: inv}
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil { if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -134,11 +130,7 @@ func TestANamedPushStillSendsAConsequenceNothingHolds(t *testing.T) {
if _, err := inv.Assign(ctx, "laptop", "resolver"); err != nil { if _, err := inv.Assign(ctx, "laptop", "resolver"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
gens, err := generators(ctx, open) compose := composeForPush(open)
if err != nil {
t.Fatal(err)
}
compose := composeForPush(open, gens)
d := &recordedDelivery{inv: inv} d := &recordedDelivery{inv: inv}
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil { if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil {
t.Fatal(err) t.Fatal(err)
-17
View File
@@ -3,7 +3,6 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -99,22 +98,6 @@ func TestAPlanWaitingOnItsFirstMachineHoldsTheRest(t *testing.T) {
} }
} }
// A send records the build of each module it carried; a module left out of it keeps the build it
// was last sent, since the machine keeps that one.
func TestASendCarriesTheCurrentBuildsAndALeftOutModuleKeepsItsOwn(t *testing.T) {
current := map[string]inventory.CurrentBuild{"a": {Commit: "a2"}, "b": {Commit: "b2"}, "c": {}}
got := carriedBuilds([]string{"a", "b", "c"}, map[string]string{"b": "a setting does not compose"},
current, map[string]string{"a": "a1", "b": "b1"})
if want := map[string]string{"a": "a2", "b": "b1", "c": ""}; !reflect.DeepEqual(got, want) {
t.Fatalf("carried %v, wanted %v", got, want)
}
// Not known before: the left-out module is not recorded at all, so it reads as never sent.
got = carriedBuilds([]string{"a", "b"}, map[string]string{"b": "x"}, current, nil)
if want := map[string]string{"a": "a2"}; !reflect.DeepEqual(got, want) {
t.Fatalf("carried %v, wanted %v", got, want)
}
}
// recordedDelivery sends nothing and records each send as the mesh does, so the next comparison // recordedDelivery sends nothing and records each send as the mesh does, so the next comparison
// reads the machine as current — and writes down which machines it declared. // reads the machine as current — and writes down which machines it declared.
type recordedDelivery struct { type recordedDelivery struct {
+1 -1
View File
@@ -63,7 +63,7 @@ func TestASecondEligibleHolderStandsBesideTheOneHeldByBeingAlone(t *testing.T) {
// And the record decides from here: the anchor's plan holds the seat, the laptop's does not. // And the record decides from here: the anchor's plan holds the seat, the laptop's does not.
for node, holds := range map[string]bool{"anchor": true, "laptop": false} { for node, holds := range map[string]bool{"anchor": true, "laptop": false} {
plan, _, err := planFor(ctx, open, node) plan, _, err := resolveNode(ctx, open, node)
if err != nil { if err != nil {
t.Fatalf("%s no longer resolves: %v", node, err) t.Fatalf("%s no longer resolves: %v", node, err)
} }
@@ -31,7 +31,7 @@ func TestAnOverflowingConsumerNeverRefusesItsProvidersMachine(t *testing.T) {
} }
// The consumer's machine resolves, and says which of its modules no provider will grant. // The consumer's machine resolves, and says which of its modules no provider will grant.
consumer, _, err := planFor(ctx, open, "laptop") consumer, _, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -42,7 +42,7 @@ func TestAnOverflowingConsumerNeverRefusesItsProvidersMachine(t *testing.T) {
// The provider's machine composes. Under ADR 0049's one bound this was a refusal naming // The provider's machine composes. Under ADR 0049's one bound this was a refusal naming
// networkmanager, and no push to the provider could go through. // networkmanager, and no push to the provider could go through.
plan, settings, err := planFor(ctx, open, "anchor") plan, settings, err := resolveNode(ctx, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -53,7 +53,7 @@ func TestAnOverflowingConsumerNeverRefusesItsProvidersMachine(t *testing.T) {
if len(declared.withheld) != 1 || declared.withheld[0].Identity != "mesh_laptop_photoalbum" { if len(declared.withheld) != 1 || declared.withheld[0].Identity != "mesh_laptop_photoalbum" {
t.Fatalf("the overflowing consumer is not the one withheld: %+v", declared.withheld) t.Fatalf("the overflowing consumer is not the one withheld: %+v", declared.withheld)
} }
grants, _, _, err := grantsFor(ctx, open, "anchor") grants, _, _, err := grantsOf(ctx, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -79,7 +79,7 @@ func TestReplay201(t *testing.T) {
t.Fatalf("the send keeps the newer controller there: %v %v", keeps, err) t.Fatalf("the send keeps the newer controller there: %v %v", keeps, err)
} }
shelf := map[string]catalogue.Manifest{"mesh-controller": {Module: "mesh-controller", Version: "old"}} shelf := map[string]catalogue.Manifest{"mesh-controller": {Module: "mesh-controller", Version: "old"}}
if _, err := keepRecorded(keepingRecorded(judged), open, "anchor", shelf); err != nil || shelf["mesh-controller"].Version != "1" { if err := applyKept(keepingRecorded(judged), open, "anchor", keeps, shelf); err != nil || shelf["mesh-controller"].Version != "1" {
t.Fatalf("the send composes the kept build's manifest: %+v %v", shelf, err) t.Fatalf("the send composes the kept build's manifest: %+v %v", shelf, err)
} }
key := coreDowngradeObservation(lease.ComponentController, "anchor", "").Key() key := coreDowngradeObservation(lease.ComponentController, "anchor", "").Key()
+71 -41
View File
@@ -6,6 +6,8 @@ import (
"testing" "testing"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose"
"git.novox.be/novox/mesh-sdk/go/overlay"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/secrets" "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") 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 // **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. // moved, so `secret issued` goes to the vault naming each holder, and its software issues the provision's credential.
func TestANeedIsIssuedToItsHolderOnlyWhereTheProviderIsMovedAndItsSoftwareIssuesIt(t *testing.T) { func TestANeedIsIssuedToItsHolderOnlyWhereTheProviderIsMovedAndItsSoftwareIssuesIt(t *testing.T) {
forge := func(moved bool, issued bool) catalogue.Manifest { 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 { if issued {
offer.Credential = &catalogue.OfferCredential{IssuedBy: catalogue.IssuedBySoftware, Tool: "gitea_agent_token"} 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 { shared := func(moved bool) catalogue.Manifest {
return catalogue.Manifest{Module: "gitea", Moved: moved, OwnSecrets: catalogue.OwnSecrets{"key": {Path: "/k"}}, return catalogue.Manifest{Module: "gitea", Version: "1", Moved: moved, OwnSecrets: catalogue.OwnSecrets{"key": {Path: "/k"}},
Provides: []catalogue.Offer{{Name: "gitea-read", Credential: &catalogue.OfferCredential{Own: "key"}}}} Provides: []catalogue.Offer{{Name: "gitea-read", Scope: catalogue.ScopeMesh, Credential: &catalogue.OfferCredential{Own: "key"}}}}
} }
given := func() catalogue.Manifest { given := func() catalogue.Manifest {
return catalogue.Manifest{Module: "gitea", Moved: true, Provides: []catalogue.Offer{{Name: "gitea-read", return catalogue.Manifest{Module: "gitea", Version: "1", Moved: true, Provides: []catalogue.Offer{{Name: "gitea-read",
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}} 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 { for why, c := range map[string]struct {
shelf map[string]catalogue.Manifest forge catalogue.Manifest
need catalogue.Needed
want bool want bool
}{ }{
"moved and issued by its software": {map[string]catalogue.Manifest{"gitea": forge(true, true)}, need, true}, "moved and issued by its software": {forge(true, true), true},
"not moved": {map[string]catalogue.Manifest{"gitea": forge(false, true)}, need, false}, "not moved": {forge(false, true), false},
"a login for each consumer": {map[string]catalogue.Manifest{"gitea": forge(true, false)}, need, true}, "a login for each consumer": {forge(true, false), true},
"a login for each, not moved": {map[string]catalogue.Manifest{"gitea": forge(false, false)}, need, false}, "a login for each, not moved": {forge(false, false), false},
"a credential every holder shares": {map[string]catalogue.Manifest{"gitea": shared(true)}, need, false}, "a credential every holder shares": {shared(true), false},
"a credential a person gives": {map[string]catalogue.Manifest{"gitea": given()}, need, false}, "a credential a person gives": {given(), 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},
} { } {
if got := issuedToHolder(c.shelf, c.need); got != c.want { got := needOf(t, map[string]catalogue.Manifest{"gitea": c.forge}, "gitea", "claude-code", "gitea-read")
t.Errorf("%s: issued to its holder is %v, want %v", why, got, c.want) 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 // 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. // marked moved; another module is not.
func TestTheModuleHoldingTheVaultSeatCountsAsMovedForItsConsumers(t *testing.T) { func TestTheModuleHoldingTheVaultSeatCountsAsMovedForItsConsumers(t *testing.T) {
vault := catalogue.Manifest{Module: "mesh-vault", Claims: []catalogue.Claim{{Name: "mesh-vault"}}, vault := catalogue.Manifest{Module: "mesh-vault", Version: "1", Claims: []catalogue.Claim{{Name: "mesh-vault", Scope: catalogue.ScopeMesh}},
Provides: []catalogue.Offer{{Name: "secret"}}} Provides: []catalogue.Offer{{Name: "secret", Scope: catalogue.ScopeMesh}}}
other := catalogue.Manifest{Module: "mesh-vault", Provides: []catalogue.Offer{{Name: "secret"}}} other := catalogue.Manifest{Module: "mesh-vault", Version: "1", Provides: []catalogue.Offer{{Name: "secret", Scope: catalogue.ScopeMesh}}}
need := catalogue.Needed{Name: "secret", Local: "admin", From: "novox", Module: "mesh-vault", For: "gitea"} if got := needOf(t, map[string]catalogue.Manifest{"mesh-vault": vault}, "mesh-vault", "gitea", "secret"); !got.IssuedToHolder || !got.ProviderMoved {
if !issuedToHolder(map[string]catalogue.Manifest{"mesh-vault": vault}, need) || t.Errorf("the vault's consumer is not answered by a moved provider: %+v", got)
!providerMoved(map[string]catalogue.Manifest{"mesh-vault": vault}, need) {
t.Error("the vault's consumer is not answered by a moved provider")
} }
if issuedToHolder(map[string]catalogue.Manifest{"mesh-vault": other}, need) || if got := needOf(t, map[string]catalogue.Manifest{"mesh-vault": other}, "mesh-vault", "gitea", "secret"); got.IssuedToHolder || got.ProviderMoved {
providerMoved(map[string]catalogue.Manifest{"mesh-vault": other}, need) { t.Errorf("a module not holding the vault seat counts as moved: %+v", got)
t.Error("a module not holding the vault seat counts as moved")
} }
} }
@@ -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 // 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. // for it. Another provision of the module, or a module without the entry, does not count.
func TestAnOfferAnsweredByTheHelperCountsAsMovedForThatProvisionOnly(t *testing.T) { func TestAnOfferAnsweredByTheHelperCountsAsMovedForThatProvisionOnly(t *testing.T) {
proxy := catalogue.Manifest{Module: "route-proxy", Provides: []catalogue.Offer{ proxy := catalogue.Manifest{Module: "route-proxy", Version: "1", Provides: []catalogue.Offer{
{Name: "route", AnsweredBy: catalogue.AnsweredByHelper}, {Name: "other"}}} {Name: "route", Scope: catalogue.ScopeMesh, AnsweredBy: catalogue.AnsweredByHelper}, {Name: "other", Scope: catalogue.ScopeMesh}}}
plain := catalogue.Manifest{Module: "route-proxy", Provides: []catalogue.Offer{{Name: "route"}}} 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 { shelf := func(m catalogue.Manifest) map[string]catalogue.Manifest {
return map[string]catalogue.Manifest{m.Module: m} return map[string]catalogue.Manifest{m.Module: m}
} }
route := catalogue.Needed{Name: "route", From: "novox", Module: "route-proxy", For: "gitea"} if got := needOf(t, shelf(proxy), "route-proxy", "gitea", "route"); !got.ProviderMoved {
other := catalogue.Needed{Name: "other", From: "novox", Module: "route-proxy", For: "gitea"}
if !providerMoved(shelf(proxy), route) {
t.Error("a provision answered by the helper is not answered by a moved provider") t.Error("a provision answered by the helper is not answered by a moved provider")
} } else if got.IssuedToHolder {
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) {
t.Error("the flag made a credential the vault's") 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")
}
} }
-18
View File
@@ -1,30 +1,12 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/overlay"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/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. // judgedNetworks is the judging alone, from what was read.
func judgedNetworks(account inventory.MachineAccount, had bool, outward []string, tunnel string) []catalogue.LocalNetwork { func judgedNetworks(account inventory.MachineAccount, had bool, outward []string, tunnel string) []catalogue.LocalNetwork {
if !had || account.Machine == nil { if !had || account.Machine == nil {
@@ -8,6 +8,7 @@ import (
"time" "time"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/link"
) )
@@ -55,8 +56,12 @@ func TestIssue969TheGateRaisesEachMachineAsItsEngineStatedItself(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
for name, hands := range want { for name, hands := range want {
got, err := engineTakesHanded(ctx, raised.inventory, name) in, err := readComposeInput(ctx, raised)
if err != nil || got != hands { 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) t.Errorf("%s: the gate raises it as taking handed files = %v (%v), the mesh says %v", name, got, err, hands)
} }
} }
@@ -14,15 +14,15 @@ func TestIssue969TheGateGivesAMachineItsContractSoItTakesHandedFiles(t *testing.
taken := time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC) taken := time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC)
stated, ok := standInHealth(snapshot.Machine{Name: "f35", Contract: link.HandedContract}, taken) 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) 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) 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) 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) 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) t.Fatalf("a snapshot that carries no contract states nothing: %+v, %v", none, ok)
} }
} }
+4 -2
View File
@@ -7,6 +7,7 @@ import (
"testing" "testing"
"time" "time"
"git.novox.be/novox/mesh-sdk/go/compose"
"github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/link"
@@ -172,11 +173,12 @@ func TestHealthIsSentOnlyToAnEngineThatSaysItReadsIt(t *testing.T) {
inv := open.inventory inv := open.inventory
reads := func() bool { reads := func() bool {
t.Helper() t.Helper()
got, err := engineReadsHealth(ctx, inv, "anchor") in, err := readComposeInput(ctx, open)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return got node, _ := nodeOf(in.Input, "anchor")
return node.Stated && node.Contract >= compose.ReadinessContract
} }
if reads() { if reads() {
t.Fatal("an engine that never stated anything is sent health") t.Fatal("an engine that never stated anything is sent health")
@@ -46,15 +46,7 @@ func TestAComposedDeclarationCarriesModulesAndBuildsOnlyForANodeThatReadsThem(t
declare := func(node string) sendable { declare := func(node string) sendable {
t.Helper() t.Helper()
plan, settings, err := planFor(ctx, open, node) d, err := composeSend(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)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
-17
View File
@@ -6,7 +6,6 @@ import (
"testing" "testing"
"git.novox.be/novox/mesh-sdk/go/validate" "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 // 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)) 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)
}
}
-129
View File
@@ -316,73 +316,6 @@ func graph(ctx context.Context, open *stores) ([]overlay.Node, overlay.Graph, er
return compose.Graph(in.Input) 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 { func overlayShow(ctx context.Context, open *stores) error {
nodes, computed, err := graph(ctx, open) nodes, computed, err := graph(ctx, open)
if err != nil { if err != nil {
@@ -576,68 +509,6 @@ func onTheNetwork(ctx context.Context, inv *inventory.Inventory,
return out, nil 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 // 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, // 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 // 04-ISSUES/102), the mesh's assignment when it made one, and the manifest's own number only when
+1 -40
View File
@@ -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 // 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. // the tunnel the hub holds — never stored anywhere else.
func TestTheOverlaysRangeIsTheAdoptedTunnels(t *testing.T) { func TestTheOverlaysRangeIsTheAdoptedTunnels(t *testing.T) {
@@ -315,16 +284,8 @@ func TestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves(t *t
func TestTheRosterNamesOnlyTheMachines(t *testing.T) { func TestTheRosterNamesOnlyTheMachines(t *testing.T) {
open := aMesh(t) open := aMesh(t)
ctx := t.Context() ctx := t.Context()
gens, err := generators(ctx, open)
if err != nil {
t.Fatal(err)
}
for _, node := range []string{"anchor", "laptop"} { for _, node := range []string{"anchor", "laptop"} {
plan, settings, err := planFor(ctx, open, node) with, err := renderingOf(ctx, open, node)
if err != nil {
t.Fatal(err)
}
with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Reading)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -5,9 +5,11 @@ package main
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"strings" "strings"
"testing" "testing"
"git.novox.be/novox/mesh-sdk/go/compose"
) )
func TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer(t *testing.T) { 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"}) }) 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) t.Fatalf("laptop resolves with two claimants of one seat: %v", err)
} }
if n := strings.Count(said, "laptop does not resolve now"); n != 1 { if n := strings.Count(said, "laptop does not resolve now"); n != 1 {
@@ -45,7 +47,7 @@ func TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if _, _, err := planFor(ctx, open, "laptop"); err != nil { if _, _, err := resolveNode(ctx, open, "laptop"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if strings.Contains(said, "does not resolve") || strings.Contains(said, "could not be read") { 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 { if err == nil {
t.Fatalf("two claimants of one node seat resolved:\n%s", said) 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() { 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) 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 // 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) { 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 var buf bytes.Buffer
sayLeftOut(leftOutSaidBy(context.Background(), &buf), "laptop", errors.New("why")) sayFound(leftOutSaidBy(context.Background(), &buf), compose.Declared{Said: []string{line}})
if buf.String() != "laptop is left out of the rest of the mesh: it does not resolve: why\n" { if buf.String() != line+"\n" {
t.Errorf("said %q", buf.String()) t.Errorf("said %q", buf.String())
} }
got := answered(t, func() error { got := answered(t, func() error {
sayLeftOut(context.Background(), "laptop", errors.New("why")) sayFound(context.Background(), compose.Declared{Said: []string{line}})
return nil return nil
}) })
if !strings.Contains(got, "laptop is left out") { if !strings.Contains(got, "laptop is left out") {
+1 -1
View File
@@ -32,7 +32,7 @@ func TestOneAcceptForAProviderIsPlacedForEveryConsumer(t *testing.T) {
} }
placed := func(module string) bool { placed := func(module string) bool {
t.Helper() t.Helper()
plan, settings, err := planFor(ctx, open, "laptop") plan, settings, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatalf("laptop was refused: %v", err) t.Fatalf("laptop was refused: %v", err)
} }
+4 -11
View File
@@ -6,6 +6,7 @@ import (
"testing" "testing"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose"
) )
// An own secret issued outside the mesh that nobody gave is never made (novox/hq issue 401, ADR 0228). Seen on // 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. // 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 { func sentTo(t *testing.T, open *stores, node string) []map[string]any {
t.Helper() t.Helper()
plan, settings, err := planFor(t.Context(), open, node) plan, settings, err := resolveNode(t.Context(), open, node)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -99,16 +100,8 @@ func TestAQuestionComposesWithoutAnOutsideSecretNobodyGave(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
pushedBy(t, open, "laptop") // the mesh's own secret is made by the send pushedBy(t, open, "laptop") // the mesh's own secret is made by the send
plan, settings, err := planFor(ctx, open, "laptop") for _, choosing := range []compose.Mode{compose.Reading, compose.Foreseeing} {
if err != nil { got, _, err := composeQuestion(ctx, open, "laptop", choosing, false)
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)
if err != nil { if err != nil {
t.Fatalf("%v: %v", choosing, err) t.Fatalf("%v: %v", choosing, err)
} }
+2 -6
View File
@@ -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"} { for _, node := range []string{"anchor", "laptop"} {
plan, settings, err := planFor(ctx, open, node) plan, _, err := resolveNode(ctx, open, node)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Allocating) with, err := madeAndRendered(ctx, open, node)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -49,15 +49,11 @@ func theWordedMesh(t *testing.T) wordedMesh {
func toldOfTheWords(t *testing.T, open *stores, node string) string { func toldOfTheWords(t *testing.T, open *stores, node string) string {
t.Helper() t.Helper()
ctx := t.Context() ctx := t.Context()
gens, err := generators(ctx, open) plan, _, err := resolveNode(ctx, open, node)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
plan, settings, err := planFor(ctx, open, node) with, err := madeAndRendered(ctx, open, node)
if err != nil {
t.Fatal(err)
}
with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Allocating)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
File diff suppressed because it is too large Load Diff
@@ -8,6 +8,7 @@ import (
"testing" "testing"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/compose"
"github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
) )
@@ -21,8 +22,9 @@ func TestTheProvisionerClientsAccountIsIssuedForItsServiceAndNeverInvented(t *te
if _, err := assign(ctx, open, "laptop", m.Module); err != nil { if _, err := assign(ctx, open, "laptop", m.Module); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := busCredentialIssued(ctx, inv, "laptop", m.Module, broker.ProvisionerSecret); err == nil { // The composition refuses it in the send's words (mesh-sdk's go/compose: TestTheSendsRefusalsAreRefusedInTheSendsWords).
t.Fatal("a push would seal a placeholder for the client's account before it was issued") if _, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false); err == nil || !strings.Contains(err.Error(), "has no bus credential") {
t.Fatalf("a push would seal a placeholder for the client's account before it was issued: %v", err)
} }
if err := issueProvisionerClient(ctx, inv, m, "laptop", broker.Broker{Fingerprint: "ab"}, "10.0.0.1:4222"); err != nil { if err := issueProvisionerClient(ctx, inv, m, "laptop", broker.Broker{Fingerprint: "ab"}, "10.0.0.1:4222"); err != nil {
@@ -38,7 +40,7 @@ func TestTheProvisionerClientsAccountIsIssuedForItsServiceAndNeverInvented(t *te
if !slices.ContainsFunc(states, func(s inventory.SecretState) bool { return s.Name == broker.ProvisionerSecret }) { if !slices.ContainsFunc(states, func(s inventory.SecretState) bool { return s.Name == broker.ProvisionerSecret }) {
t.Fatalf("the client's account is not sealed to the machine for its service: %+v", states) t.Fatalf("the client's account is not sealed to the machine for its service: %+v", states)
} }
if err := busCredentialIssued(ctx, inv, "laptop", m.Module, broker.ProvisionerSecret); err != nil { if _, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false); err != nil && strings.Contains(err.Error(), "has no bus credential") {
t.Fatalf("an issued client account is refused: %v", err) t.Fatalf("an issued client account is refused: %v", err)
} }
if !runsTheProvisionerClient(m) { if !runsTheProvisionerClient(m) {
+3 -3
View File
@@ -522,8 +522,8 @@ func pushCommand(ctx context.Context, args []string) error {
return nil return nil
} }
} }
gens, err := generatorsOf(ctx, open) // The private network every declaration of the push is composed with: not computable refuses the push up front.
if err != nil { if _, err := generatorsOf(ctx, open); err != nil {
return err return err
} }
@@ -724,7 +724,7 @@ func pushCommand(ctx context.Context, args []string) error {
keep := keeping(func(held context.Context, sending []readyNode) ([]readyNode, error) { keep := keeping(func(held context.Context, sending []readyNode) ([]readyNode, error) {
return holdingBack(held, inv, sending, movable, &heldBack) return holdingBack(held, inv, sending, movable, &heldBack)
}) })
refused, err := flushBehind(ctx, open, nodes, handled, composeForPush(open, gens), bus, holder, keep, os.Stdout) refused, err := flushBehind(ctx, open, nodes, handled, composeForPush(open), bus, holder, keep, os.Stdout)
refusals = append(refusals, refused...) refusals = append(refusals, refused...)
if err != nil { if err != nil {
return err return err
+1 -9
View File
@@ -48,15 +48,7 @@ func TestAPushSaysWhatItRecreates(t *testing.T) {
} }
// Composed as a person's push composes it. // Composed as a person's push composes it.
plan, settings, err := planFor(ctx, open, "anchor") declared, err := composeSend(ctx, open, "anchor")
if err != nil {
t.Fatal(err)
}
gens, err := generators(ctx, open)
if err != nil {
t.Fatal(err)
}
declared, err := declarationWith(ctx, open, "anchor", plan, settings, gens, Allocating)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
-10
View File
@@ -180,16 +180,6 @@ func recordedKept(ctx context.Context, open *stores, node string) (map[string]st
return out, nil return out, nil
} }
// keepRecorded puts, in a shelf about to be resolved for a machine, the build each recorded module there
// runs in place of the one the mesh holds; it answers what it kept, module → commit.
func keepRecorded(ctx context.Context, open *stores, node string, shelf map[string]catalogue.Manifest) (map[string]string, error) {
kept, err := recordedKept(ctx, open, node)
if err != nil || len(kept) == 0 {
return nil, err
}
return kept, applyKept(ctx, open, node, kept, shelf)
}
// applyKept puts the manifest of each module kept at the build its machine runs into a shelf, and refuses, in the words of // applyKept puts the manifest of each module kept at the build its machine runs into a shelf, and refuses, in the words of
// keepRecorded, a build the records no longer hold. The adapter to the composition's input (compose_input.go) calls it with // keepRecorded, a build the records no longer hold. The adapter to the composition's input (compose_input.go) calls it with
// a scratch shelf, to read the manifests it gives the node as its own Kept. // a scratch shelf, to read the manifests it gives the node as its own Kept.
@@ -63,7 +63,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
// A plan's, a release plan's, a healer's send: the database is kept as it runs, mail moves. // A plan's, a release plan's, a healer's send: the database is kept as it runs, mail moves.
kept := keepingRecorded(ctx) kept := keepingRecorded(ctx)
plan, settings, err := planFor(kept, open, "anchor") plan, _, err := resolveNode(kept, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -76,11 +76,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
if imageOf(t, plan, "postgres", "server") != pgImage { if imageOf(t, plan, "postgres", "server") != pgImage {
t.Fatal("the recorded module's container lost its image") t.Fatal("the recorded module's container lost its image")
} }
gens, err := generators(kept, open) declared, err := composeSend(kept, open, "anchor")
if err != nil {
t.Fatal(err)
}
declared, err := declarationWith(kept, open, "anchor", plan, settings, gens, Allocating)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -89,7 +85,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
} }
// A person's push: the recorded module's new build. // A person's push: the recorded module's new build.
plan, _, err = planFor(ctx, open, "anchor") plan, _, err = resolveNode(ctx, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -98,7 +94,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
} }
// The bus step moves the bus alone: a recorded module it is told it may move moves, the others stay. // The bus step moves the bus alone: a recorded module it is told it may move moves, the others stay.
plan, _, err = planFor(keepingRecorded(ctx, "postgres"), open, "anchor") plan, _, err = resolveNode(keepingRecorded(ctx, "postgres"), open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -128,7 +124,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
map[string]string{"postgres": "c0000000", "mailu": "c2222222"}); err != nil { map[string]string{"postgres": "c0000000", "mailu": "c2222222"}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if _, _, err := planFor(kept, open, "anchor"); err == nil || !strings.Contains(err.Error(), "push anchor") { if _, _, err := resolveNode(kept, open, "anchor"); err == nil || !strings.Contains(err.Error(), "push anchor") {
t.Fatalf("a recorded build that cannot be kept did not refuse the send with its remedy: %v", err) t.Fatalf("a recorded build that cannot be kept did not refuse the send with its remedy: %v", err)
} }
+1 -1
View File
@@ -349,7 +349,7 @@ func TestARecordedMoveThatCannotBeJudgedWaits(t *testing.T) {
!strings.Contains(v.said, "not in the build records") { !strings.Contains(v.said, "not in the build records") {
t.Fatalf("an unjudgeable move: %+v", v) t.Fatalf("an unjudgeable move: %+v", v)
} }
if _, _, err := planFor(keepingRecorded(t.Context()), rm.open, "anchor"); err == nil || !strings.Contains(err.Error(), "push anchor") { if _, _, err := resolveNode(keepingRecorded(t.Context()), rm.open, "anchor"); err == nil || !strings.Contains(err.Error(), "push anchor") {
t.Fatalf("a send that cannot keep the unjudged build did not refuse with its remedy: %v", err) t.Fatalf("a send that cannot keep the unjudged build did not refuse with its remedy: %v", err)
} }
// laptop, judged from c1, goes; anchor waits. // laptop, judged from c1, goes; anchor waits.
+1 -1
View File
@@ -34,7 +34,7 @@ func TestReplay164(t *testing.T) {
} }
sent := func() []map[string]any { sent := func() []map[string]any {
t.Helper() t.Helper()
plan, settings, err := planFor(ctx, open, "laptop") plan, settings, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatalf("the consumer's machine was refused for a credential only a person gives: %v", err) t.Fatalf("the consumer's machine was refused for a credential only a person gives: %v", err)
} }
+1 -1
View File
@@ -71,7 +71,7 @@ func TestReplay201ThePlansSend(t *testing.T) {
if keeping { if keeping {
c = keepingRecorded(ctx) c = keepingRecorded(ctx)
} }
plan, settings, err := planFor(c, open, "anchor") plan, settings, err := resolveNode(c, open, "anchor")
if err != nil { if err != nil {
t.Fatalf("%s to anchor did not compose: %v", sends, err) t.Fatalf("%s to anchor did not compose: %v", sends, err)
} }
+1 -1
View File
@@ -34,7 +34,7 @@ func TestReplay237(t *testing.T) {
said := answered(t, func() error { return assignCommand(ctx, "assign", []string{"laptop", "docker"}) }) said := answered(t, func() error { return assignCommand(ctx, "assign", []string{"laptop", "docker"}) })
if _, _, err := planFor(ctx, open, "laptop"); err != nil { if _, _, err := resolveNode(ctx, open, "laptop"); err != nil {
t.Fatalf("laptop does not resolve right after the act: %v\nthe answer was:\n%s", err, said) t.Fatalf("laptop does not resolve right after the act: %v\nthe answer was:\n%s", err, said)
} }
if !strings.Contains(said, "web on laptop now has "+catalogue.ContainerRuntimeSeat+" held") { if !strings.Contains(said, "web on laptop now has "+catalogue.ContainerRuntimeSeat+" held") {
+1 -1
View File
@@ -33,7 +33,7 @@ func TestReplay401(t *testing.T) {
} }
sent := func() []map[string]any { sent := func() []map[string]any {
t.Helper() t.Helper()
plan, settings, err := planFor(ctx, open, "laptop") plan, settings, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -35,7 +35,7 @@ func TestReplay263AnIdentityTooLongNeverRefusesItsProvidersMachine(t *testing.T)
} }
// The consumer's machine composes first, as a push does: what it is sent is what its provider grants. // The consumer's machine composes first, as a push does: what it is sent is what its provider grants.
for _, node := range []string{"laptop", "anchor"} { for _, node := range []string{"laptop", "anchor"} {
plan, settings, err := planFor(ctx, open, node) plan, settings, err := resolveNode(ctx, open, node)
if err != nil { if err != nil {
t.Fatalf("%s does not resolve: %v", node, err) t.Fatalf("%s does not resolve: %v", node, err)
} }
@@ -84,7 +84,7 @@ func TestReplay273AConsumerBesideItsStoreStaysBoundToIt(t *testing.T) {
assignAll([2]string{"laptop", "store"}, [2]string{"laptop", "resolver"}, [2]string{"laptop", "network"}, assignAll([2]string{"laptop", "store"}, [2]string{"laptop", "resolver"}, [2]string{"laptop", "network"},
[2]string{"laptop", "board"}) [2]string{"laptop", "board"})
plan, _, err := planFor(ctx, open, "laptop") plan, _, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+2 -2
View File
@@ -22,7 +22,7 @@ func TestASetThatDoesNotComposeIsMarkedAsTheNodesOwnProblem(t *testing.T) {
} }
} }
_, _, err := planFor(t.Context(), open, "laptop") _, _, err := resolveNode(t.Context(), open, "laptop")
if err == nil { if err == nil {
t.Fatal("two modules claiming one seat composed anyway") t.Fatal("two modules claiming one seat composed anyway")
} }
@@ -38,7 +38,7 @@ func TestAStoreThatCannotBeReadIsNotANodeThatDoesNotCompose(t *testing.T) {
stopped, cancel := context.WithCancel(t.Context()) stopped, cancel := context.WithCancel(t.Context())
cancel() cancel()
_, _, err := planFor(stopped, open, "anchor") _, _, err := resolveNode(stopped, open, "anchor")
if err == nil { if err == nil {
t.Fatal("a plan composed against a store that could not be read") t.Fatal("a plan composed against a store that could not be read")
} }
+21 -69
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"os"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
@@ -13,24 +12,12 @@ import (
"github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/link"
) )
// seatPassing is what passing seats on needs of the outside: the record's compare-and-set, and the hand act. // performSeatPasses records, in the send that finds them, the new holder of each seat held once whose holder on record is
type seatPassing struct { // assigned nowhere and which has exactly one claimant able to hold it (the composition names them: catalogue.SeatsToPassOn,
pass func(ctx context.Context, p catalogue.SeatPass) (bool, error) // novox/hq ADR 0449 §4, issue 1317), and writes the controller's own hand act for each. A failed write is said and leaves the
act func(ctx context.Context, p catalogue.SeatPass) error // record as it was: a send is never refused for it. It answers whether it recorded any.
say io.Writer func performSeatPasses(ctx context.Context, sp seatPassing, passes []catalogue.SeatPass) bool {
} did := false
// passSeatsOn records, in the composition that finds it, the new holder of each seat held once whose holder on
// record is assigned nowhere and which has exactly one claimant able to hold it, and writes the controller's own
// hand act for it (novox/hq ADR 0449 §4, issue 1317). It answers the holdings as they are after it, so the
// resolution that follows reads the record it just moved. A failed write is said and leaves the record as it was:
// a composition is never refused for it.
func passSeatsOn(ctx context.Context, sp seatPassing, holdings []catalogue.Held,
assignments []catalogue.Assignment) []catalogue.Held {
passes, said := catalogue.SeatsToPassOn(holdings, assignments)
for _, line := range said {
fmt.Fprintln(sp.say, line)
}
for _, p := range passes { for _, p := range passes {
moved, err := sp.pass(ctx, p) moved, err := sp.pass(ctx, p)
if err != nil { if err != nil {
@@ -41,21 +28,20 @@ func passSeatsOn(ctx context.Context, sp seatPassing, holdings []catalogue.Held,
if !moved { if !moved {
continue // another composition passed it first, and wrote the act continue // another composition passed it first, and wrote the act
} }
did = true
if err := sp.act(ctx, p); err != nil { if err := sp.act(ctx, p); err != nil {
fmt.Fprintf(sp.say, "%s passed from %s on %s to %s on %s, and the hand act could NOT be recorded: %v\n", fmt.Fprintf(sp.say, "%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) p.Seat.Name, p.From.Module, p.From.Node, p.To.Module, p.To.Node, err)
} }
var next []catalogue.Held
for _, h := range holdings {
if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == p.Seat.Name {
continue
}
next = append(next, h)
}
p.To.Site = p.From.Site
holdings = append(next, p.To)
} }
return holdings return did
}
// seatPassing is what passing seats on needs of the outside: the record's compare-and-set, and the hand act.
type seatPassing struct {
pass func(ctx context.Context, p catalogue.SeatPass) (bool, error)
act func(ctx context.Context, p catalogue.SeatPass) error
say io.Writer
} }
// seatPassHandAct is the controller's own entry for a pass: it is the act that puts the holder on record, // seatPassHandAct is the controller's own entry for a pass: it is the act that puts the holder on record,
@@ -73,31 +59,10 @@ func seatPassHandAct(p catalogue.SeatPass) link.HandAct {
} }
} }
// assignmentsOf are the assignments of every machine, as the passing of a seat reads them. **A module assigned and // seatPassingOn is how a send passes a seat: against the store's record, with the hand act on the bus; on a throwaway mesh
// missing from the shelf is still assigned**: it is an assignment with no claims, so its holder is not taken for gone. // (the merge gate's) the record is kept in its store and no act is written, for there is no bus to write it to.
func assignmentsOf(shelf map[string]catalogue.Manifest, assigned map[string][]string) []catalogue.Assignment {
var out []catalogue.Assignment
for node, modules := range assigned {
for _, module := range modules {
m, ok := shelf[module]
if !ok {
m = catalogue.Manifest{Module: module}
}
out = append(out, catalogue.Assignment{Node: node, Manifest: m})
}
}
return out
}
// seatPassingOn is how a composition on this inventory passes a seat: against the store's record with the hand act
// on the bus, or, on a throwaway mesh (the merge gate's), in memory only, writing nothing anywhere.
func seatPassingOn(inv *inventory.Inventory, w io.Writer) seatPassing { func seatPassingOn(inv *inventory.Inventory, w io.Writer) seatPassing {
if inv.Throwaway() { sp := seatPassing{
return seatPassing{say: w,
pass: func(context.Context, catalogue.SeatPass) (bool, error) { return true, nil },
act: func(context.Context, catalogue.SeatPass) error { return nil }}
}
return seatPassing{
say: w, say: w,
pass: func(ctx context.Context, p catalogue.SeatPass) (bool, error) { pass: func(ctx context.Context, p catalogue.SeatPass) (bool, error) {
return inv.PassSeat(ctx, p.Seat.Name, p.Seat.Scope, p.From, p.To) return inv.PassSeat(ctx, p.Seat.Name, p.Seat.Scope, p.From, p.To)
@@ -109,21 +74,8 @@ func seatPassingOn(inv *inventory.Inventory, w io.Writer) seatPassing {
}) })
}, },
} }
} if inv.Throwaway() {
sp.act = func(context.Context, catalogue.SeatPass) error { return nil }
// theMeshPassesSeatsOn is passSeatsOn as the composition runs it. **Only when every machine's assignments were
// read** (assignedErr nil): a machine whose read failed would look as if its modules were assigned nowhere, and
// its holder would lose the seat. Said to where the composition says its other lines (sayLeftOut).
func theMeshPassesSeatsOn(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest,
assigned map[string][]string, assignedErr error, holdings []catalogue.Held) []catalogue.Held {
var w io.Writer = os.Stderr
if lw, ok := ctx.Value(leftOutKey{}).(io.Writer); ok {
w = lw
} }
if assignedErr != nil { return sp
fmt.Fprintf(w, "no seat is passed on in this composition: a machine's assignments could not be read, so a "+
"holder on record might look assigned nowhere: %v\n", assignedErr)
return holdings
}
return passSeatsOn(ctx, seatPassingOn(inv, w), holdings, assignmentsOf(shelf, assigned))
} }
+35 -91
View File
@@ -26,9 +26,16 @@ func withBusSeat(t *testing.T) {
catalogue.UseSeats([]catalogue.Seat{{Name: "mesh-broker", Scope: catalogue.ScopeMesh, Delivers: "mesh-bus", Decision: "test"}}) catalogue.UseSeats([]catalogue.Seat{{Name: "mesh-broker", Scope: catalogue.ScopeMesh, Delivers: "mesh-bus", Decision: "test"}})
} }
// 1317/AC1: the composition records the claimant as the holder and writes one hand act by the controller naming // passesFor is what the composition names to pass on for these holdings and assignments (mesh-sdk's SeatsToPassOn), with the
// the seat, the former holder and the new one; the holdings it answers name the new holder. // lines it says.
func TestACompositionPassesTheSeatAndWritesOneHandActByTheController(t *testing.T) { func passesFor(holdings []catalogue.Held, as []catalogue.Assignment) ([]catalogue.SeatPass, []string) {
return catalogue.SeatsToPassOn(holdings, as)
}
// 1317/AC1: the send records the claimant as the holder and writes one hand act by the controller naming the seat, the former
// holder and the new one. (Which seats to pass, and the holdings after it, are mesh-sdk's: go/compose rest_test.go
// TestASeatWhoseHolderIsAssignedNowhereGoesToItsOnlyClaimant and TestAHolderOnRecordSettlesTheSeatInTheRestOfTheMesh.)
func TestASendPassesTheSeatAndWritesOneHandActByTheController(t *testing.T) {
withBusSeat(t) withBusSeat(t)
var passed []catalogue.SeatPass var passed []catalogue.SeatPass
var acts []link.HandAct var acts []link.HandAct
@@ -44,9 +51,9 @@ func TestACompositionPassesTheSeatAndWritesOneHandActByTheController(t *testing.
}, },
} }
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}} holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}
got := passSeatsOn(t.Context(), sp, holdings, []catalogue.Assignment{{Node: "novox", Manifest: aBrokerClaiming("gate")}}) passes, _ := passesFor(holdings, []catalogue.Assignment{{Node: "novox", Manifest: aBrokerClaiming("gate")}})
if len(got) != 1 || got[0].Module != "gate" || got[0].Node != "novox" { if !performSeatPasses(t.Context(), sp, passes) {
t.Fatalf("the holdings after the composition do not name the gate: %+v", got) t.Fatal("a pass was recorded and the send does not say it made something")
} }
if len(passed) != 1 || len(acts) != 1 { if len(passed) != 1 || len(acts) != 1 {
t.Fatalf("passes=%d acts=%d, want one each", len(passed), len(acts)) t.Fatalf("passes=%d acts=%d, want one each", len(passed), len(acts))
@@ -60,67 +67,35 @@ func TestACompositionPassesTheSeatAndWritesOneHandActByTheController(t *testing.
} }
} }
// 1317/AC2: two claimants, nothing recorded, the refusal said. // A send that lost the compare-and-set writes no act, and a failed write leaves the record.
func TestACompositionWithTwoClaimantsRecordsNoneAndSaysSo(t *testing.T) {
withBusSeat(t)
sp := seatPassing{say: &bytes.Buffer{},
pass: func(context.Context, catalogue.SeatPass) (bool, error) {
t.Fatal("a seat with two claimants was passed")
return false, nil
},
act: func(context.Context, catalogue.SeatPass) error { t.Fatal("an act was written"); return nil },
}
var said bytes.Buffer
sp.say = &said
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}
got := passSeatsOn(t.Context(), sp, holdings, []catalogue.Assignment{
{Node: "novox", Manifest: aBrokerClaiming("gate")}, {Node: "ace", Manifest: aBrokerClaiming("other")}})
if len(got) != 1 || got[0].Module != "old-broker" {
t.Fatalf("the record changed: %+v", got)
}
if !strings.Contains(said.String(), "gate") || !strings.Contains(said.String(), "other") ||
!strings.Contains(said.String(), "seat mesh-broker --to") {
t.Fatalf("the refusal is not said with both and the remedy: %q", said.String())
}
}
// 1317/AC3 and AC4: nothing changes while the holder is assigned, nor with no claimant.
func TestACompositionChangesNothingWhileTheHolderIsAssignedOrNobodyClaims(t *testing.T) {
withBusSeat(t)
sp := seatPassing{say: &bytes.Buffer{},
pass: func(context.Context, catalogue.SeatPass) (bool, error) { t.Fatal("passed"); return false, nil },
act: func(context.Context, catalogue.SeatPass) error { t.Fatal("act"); return nil },
}
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}
got := passSeatsOn(t.Context(), sp, holdings, []catalogue.Assignment{
{Node: "novox", Manifest: aBrokerClaiming("old-broker")}, {Node: "novox", Manifest: aBrokerClaiming("gate")}})
if len(got) != 1 || got[0].Module != "old-broker" {
t.Fatalf("holder assigned: record changed: %+v", got)
}
got = passSeatsOn(t.Context(), sp, holdings, nil)
if len(got) != 1 || got[0].Module != "old-broker" {
t.Fatalf("no claimant: record changed: %+v", got)
}
}
// A composition that lost the compare-and-set writes no act, and a failed write leaves the record.
func TestALostPassWritesNoActAndAFailedOneLeavesTheRecord(t *testing.T) { func TestALostPassWritesNoActAndAFailedOneLeavesTheRecord(t *testing.T) {
withBusSeat(t) withBusSeat(t)
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}} holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}
as := []catalogue.Assignment{{Node: "novox", Manifest: aBrokerClaiming("gate")}} as := []catalogue.Assignment{{Node: "novox", Manifest: aBrokerClaiming("gate")}}
passes, _ := passesFor(holdings, as)
lost := seatPassing{say: &bytes.Buffer{}, lost := seatPassing{say: &bytes.Buffer{},
pass: func(context.Context, catalogue.SeatPass) (bool, error) { return false, nil }, pass: func(context.Context, catalogue.SeatPass) (bool, error) { return false, nil },
act: func(context.Context, catalogue.SeatPass) error { t.Fatal("act without a pass"); return nil }, act: func(context.Context, catalogue.SeatPass) error { t.Fatal("act without a pass"); return nil },
} }
passSeatsOn(t.Context(), lost, holdings, as) if performSeatPasses(t.Context(), lost, passes) {
t.Fatal("a lost pass is said to have made something")
}
var said bytes.Buffer var said bytes.Buffer
sp := seatPassing{say: &said, sp := seatPassing{say: &said,
pass: func(context.Context, catalogue.SeatPass) (bool, error) { return false, context.DeadlineExceeded }, pass: func(context.Context, catalogue.SeatPass) (bool, error) { return false, context.DeadlineExceeded },
act: func(context.Context, catalogue.SeatPass) error { t.Fatal("act"); return nil }, act: func(context.Context, catalogue.SeatPass) error { t.Fatal("act"); return nil },
} }
got := passSeatsOn(t.Context(), sp, holdings, as) if performSeatPasses(t.Context(), sp, passes) || !strings.Contains(said.String(), "could not be passed") {
if got[0].Module != "old-broker" || !strings.Contains(said.String(), "could not be passed") { t.Fatalf("a failed pass is not said: %q", said.String())
t.Fatalf("got %+v said %q", got, said.String()) }
// And an act that cannot be written is said, with the record kept.
var told bytes.Buffer
kept := seatPassing{say: &told,
pass: func(context.Context, catalogue.SeatPass) (bool, error) { return true, nil },
act: func(context.Context, catalogue.SeatPass) error { return errors.New("no bus") },
}
if !performSeatPasses(t.Context(), kept, passes) || !strings.Contains(told.String(), "the hand act could NOT be recorded") {
t.Fatalf("a failed act is not said: %q", told.String())
} }
} }
@@ -146,46 +121,15 @@ func TestTwoSeatPassesAreNoRepairAndRaiseNoHealerWanted(t *testing.T) {
} }
} }
// A failed read of one machine's assignments passes no seat: its holder would look assigned nowhere. // On the merge gate's throwaway mesh the pass is recorded in its store and no hand act is written: there is no bus to write
func TestNoSeatIsPassedWhenAMachinesAssignmentsCouldNotBeRead(t *testing.T) { // it to.
withBusSeat(t) func TestAThrowawayMeshWritesNoHandActForAPass(t *testing.T) {
var said bytes.Buffer
ctx := leftOutSaidBy(t.Context(), &said)
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "ace", Module: "old-broker"}}
shelf := map[string]catalogue.Manifest{"gate": aBrokerClaiming("gate")}
got := theMeshPassesSeatsOn(ctx, nil, shelf, map[string][]string{"novox": {"gate"}},
errors.New("ace: the bus did not answer"), holdings)
if len(got) != 1 || got[0].Module != "old-broker" {
t.Fatalf("the record moved: %+v", got)
}
if !strings.Contains(said.String(), "no seat is passed") || !strings.Contains(said.String(), "ace") {
t.Errorf("not said: %q", said.String())
}
}
// A holder that is assigned but missing from the shelf is still assigned.
func TestAHolderAssignedButNotOnTheShelfIsNotTakenForGone(t *testing.T) {
withBusSeat(t)
as := assignmentsOf(map[string]catalogue.Manifest{"gate": aBrokerClaiming("gate")},
map[string][]string{"novox": {"gate", "old-broker"}})
passes, _ := catalogue.SeatsToPassOn(
[]catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}, as)
if len(passes) != 0 {
t.Fatalf("a holder missing from the shelf lost its seat: %+v", passes)
}
}
// The merge gate's throwaway mesh passes the seat in memory and writes nothing: no record, no hand act.
func TestAThrowawayMeshPassesTheSeatInMemoryAndWritesNothing(t *testing.T) {
withBusSeat(t)
inv := &inventory.Inventory{} inv := &inventory.Inventory{}
inv.MarkThrowaway() inv.MarkThrowaway()
var said bytes.Buffer var said bytes.Buffer
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}} sp := seatPassingOn(inv, &said)
got := passSeatsOn(t.Context(), seatPassingOn(inv, &said), holdings, if err := sp.act(t.Context(), catalogue.SeatPass{}); err != nil {
[]catalogue.Assignment{{Node: "novox", Manifest: aBrokerClaiming("gate")}}) t.Fatalf("the throwaway mesh tried to write the act: %v", err)
if len(got) != 1 || got[0].Module != "gate" {
t.Fatalf("the gate's view does not hold the pass: %+v", got)
} }
if said.Len() != 0 { if said.Len() != 0 {
t.Errorf("the throwaway composition said %q (a write was tried)", said.String()) t.Errorf("the throwaway composition said %q (a write was tried)", said.String())
-53
View File
@@ -1,11 +1,8 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
@@ -168,53 +165,3 @@ func (s sendable) Body() ([]byte, error) {
} }
return json.Marshal(envelope) return json.Marshal(envelope)
} }
// adoptionOf is the envelope for a node, nil when it is converged.
func adoptionOf(ctx context.Context, inv *inventory.Inventory, record inventory.Node,
plan catalogue.Resolution, composed catalogue.Composed) (*adoptionEnvelope, error) {
if !record.Adopted {
return nil, nil
}
taken, err := inv.Taken(ctx, record.Name)
if err != nil {
return nil, err
}
return adoptionFor(plan, taken, composed), nil
}
// adoptionFor is the envelope computed from what was taken and who owns each resource.
//
// Every module the node runs that is not taken is untaken — including one pulled in by another
// rather than assigned: what is found is kept until its module is taken, whoever put it there.
func adoptionFor(plan catalogue.Resolution, taken []string,
composed catalogue.Composed) *adoptionEnvelope {
isTaken := map[string]bool{}
for _, m := range taken {
isTaken[m] = true
}
out := &adoptionEnvelope{Taken: []string{}}
runs := map[string]bool{}
for _, m := range plan.Modules {
runs[m.Module] = true
if isTaken[m.Module] {
out.Taken = append(out.Taken, m.Module)
}
}
sort.Strings(out.Taken)
for _, r := range composed.Resources {
id, _ := r["id"].(string)
module, owned := composed.Owner[id]
// Every kind, not only files and containers (novox/hq ADR 0103): a directory, a service, a
// container mounting what was found and an action run in a held container all reach what
// the machine already has. What the mesh declares of its own is never held.
if !owned || !runs[module] || isTaken[module] ||
strings.HasPrefix(id, catalogue.AdoptionPrefix) {
continue
}
if out.Untaken == nil {
out.Untaken = map[string][]string{}
}
out.Untaken[module] = append(out.Untaken[module], id)
}
return out
}
@@ -157,7 +157,7 @@ func TestConsumersAreToldTheGivenPort(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
plan, _, err := planFor(ctx, open, "laptop") plan, _, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -183,7 +183,7 @@ func TestConsumersAreToldTheGivenPort(t *testing.T) {
!strings.Contains(err.Error(), "per node") { !strings.Contains(err.Error(), "per node") {
t.Fatalf("a port given for the whole mesh was not refused: %v", err) t.Fatalf("a port given for the whole mesh was not refused: %v", err)
} }
plan, settings, err := planFor(ctx, open, "anchor") plan, settings, err := resolveNode(ctx, open, "anchor")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -283,7 +283,7 @@ func TestTheMachineSideOfAMappingIsMovedEverywhereTheNumberIsUsed(t *testing.T)
} }
// And the consumer on the other machine dials the same number. // And the consumer on the other machine dials the same number.
plan, _, err := planFor(ctx, open, "laptop") plan, _, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+1 -1
View File
@@ -26,7 +26,7 @@ func helloWeb() catalogue.Manifest {
// composed is what node would be sent now, as push composes it. // composed is what node would be sent now, as push composes it.
func composed(t *testing.T, open *stores, node string) sendable { func composed(t *testing.T, open *stores, node string) sendable {
t.Helper() t.Helper()
plan, settings, err := planFor(t.Context(), open, node) plan, settings, err := resolveNode(t.Context(), open, node)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -38,7 +38,7 @@ func TestAModulePulledInIsHandedOnAMachineThatCanTakeItAndMirrored(t *testing.T)
t.Fatal(err) t.Fatal(err)
} }
plan, settings, err := planFor(ctx, open, "laptop") plan, settings, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+2 -2
View File
@@ -36,7 +36,7 @@ func TestAProviderElsewhereServesItsOwnComposedName(t *testing.T) {
if err := open.inventory.SetPublicDomain(ctx, "anchor", "example.test"); err != nil { if err := open.inventory.SetPublicDomain(ctx, "anchor", "example.test"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
plan, _, err := planFor(ctx, open, "laptop") plan, _, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -48,7 +48,7 @@ func TestAProviderElsewhereServesItsOwnComposedName(t *testing.T) {
if err := open.inventory.SetPublicDomain(ctx, "anchor", ""); err != nil { if err := open.inventory.SetPublicDomain(ctx, "anchor", ""); err != nil {
t.Fatal(err) t.Fatal(err)
} }
plan, _, err = planFor(ctx, open, "laptop") plan, _, err = resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+2 -2
View File
@@ -24,7 +24,7 @@ func TestPlanAndNodeListAnOptionalRequirementLeftUnbound(t *testing.T) {
t.Fatalf("grafana could not be assigned on a mesh without influxdb: %v", err) t.Fatalf("grafana could not be assigned on a mesh without influxdb: %v", err)
} }
plan, _, err := planFor(ctx, open, "laptop") plan, _, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -44,7 +44,7 @@ func TestPlanAndNodeListAnOptionalRequirementLeftUnbound(t *testing.T) {
if _, err := assign(ctx, open, "anchor", "influxdb"); err != nil { if _, err := assign(ctx, open, "anchor", "influxdb"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
plan, _, err = planFor(ctx, open, "laptop") plan, _, err = resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+1 -1
View File
@@ -271,7 +271,7 @@ func TestAnIssuedValueTakesThePathOfItsProvidersModule(t *testing.T) {
} }
} }
// The consumer's send makes the pair, so the provision has a holder to take it for. // The consumer's send makes the pair, so the provision has a holder to take it for.
plan, settings, err := planFor(ctx, open, "laptop") plan, settings, err := resolveNode(ctx, open, "laptop")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }