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 isfb556d6b, 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 at198be8217c, built fromfb556d6bby patch commits and compared with the author's worktree), so that this commit's parent isfb556d6b.
This commit is contained in:
@@ -13,7 +13,6 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
"git.novox.be/novox/mesh-sdk/go/overlay"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
@@ -708,18 +707,10 @@ func converge(ctx context.Context, open *stores, node string, yes bool, digest s
|
||||
}
|
||||
|
||||
// What the filter is derived from: the rendering of this machine as a question reads it, with the private network.
|
||||
input, err := inputFor(ctx, open)
|
||||
with, err := renderingOf(ctx, open, node)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rendered, err := compose.Render(input.Input, node, compose.Reading)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
with := rendered.Rendering
|
||||
if with.Generators, err = compose.Generators(input.Input); err != nil {
|
||||
return "", err
|
||||
}
|
||||
rules, err := plan.Rules(with)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
"git.novox.be/novox/mesh-sdk/go/overlay"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
@@ -25,21 +26,13 @@ func TestAModuleAssignedAndNotPushedIsAwaitingAPushNotUncomposable(t *testing.T)
|
||||
|
||||
// The read the rest of the mesh asks still refuses it, with the composer's typed error: nothing
|
||||
// can be compared about a secret that does not exist (status), and nothing here string-matches.
|
||||
plan, settings, err := planFor(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gens, err := generators(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = declarationWith(ctx, open, "laptop", plan, settings, gens, Reading)
|
||||
_, _, err := composeQuestion(ctx, open, "laptop", compose.Reading, false)
|
||||
var notMade *catalogue.NotMadeError
|
||||
if !errors.As(err, ¬Made) || notMade.Module != "keeper" || notMade.Name != "repository" {
|
||||
t.Fatalf("a read composition did not say which secret is not made, typed: %v", err)
|
||||
}
|
||||
// Foreseen, it composes, names what the push makes, and made nothing.
|
||||
foreseen, err := declarationWith(ctx, open, "laptop", plan, settings, gens, Foreseeing)
|
||||
foreseen, _, err := composeQuestion(ctx, open, "laptop", compose.Foreseeing, false)
|
||||
if err != nil || len(foreseen.foreseen) != 1 || foreseen.foreseen[0] != "keeper/repository" {
|
||||
t.Fatalf("foreseen: %v %v", foreseen.foreseen, err)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func aKeeper() catalogue.Manifest {
|
||||
func pushedBy(t *testing.T, open *stores, node string) string {
|
||||
t.Helper()
|
||||
ctx := t.Context()
|
||||
plan, settings, err := planFor(ctx, open, node)
|
||||
plan, settings, err := resolveNode(ctx, open, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -54,11 +54,10 @@ func pushedBy(t *testing.T, open *stores, node string) string {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
names := make([]string, 0, len(plan.Modules))
|
||||
builds = map[string]string{}
|
||||
for _, m := range plan.Modules {
|
||||
names = append(names, m.Module)
|
||||
builds[m.Module] = current[m.Module].Commit
|
||||
}
|
||||
builds = carriedBuilds(names, nil, current, nil)
|
||||
}
|
||||
if err := open.inventory.RecordSent(ctx, record.ID, digest, builds); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestTheIncidentAConsumerStaysBesideItsDataUntilAPersonPinsIt(t *testing.T)
|
||||
assignAll([2]string{"laptop", "store"}, [2]string{"laptop", "resolver"}, [2]string{"laptop", "network"},
|
||||
[2]string{"laptop", "board"})
|
||||
|
||||
plan, _, err := planFor(ctx, open, "laptop")
|
||||
plan, _, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func TestTheIncidentAConsumerStaysBesideItsDataUntilAPersonPinsIt(t *testing.T)
|
||||
if err := inv.Unassign(ctx, "laptop", "store"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := planFor(ctx, open, "laptop"); err == nil || !unresolvable(err) ||
|
||||
if _, _, err := resolveNode(ctx, open, "laptop"); err == nil || !unresolvable(err) ||
|
||||
!strings.Contains(err.Error(), "board on laptop is bound to laptop/store") ||
|
||||
!strings.Contains(err.Error(), "pin laptop postgres-database anchor store") {
|
||||
t.Fatalf("the consumer's store went and it was answered elsewhere: %v", err)
|
||||
@@ -74,7 +74,7 @@ func TestTheIncidentAConsumerStaysBesideItsDataUntilAPersonPinsIt(t *testing.T)
|
||||
if err := inv.PinProvision(ctx, "laptop", "postgres-database", "anchor", "store"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plan, _, err = planFor(ctx, open, "laptop")
|
||||
plan, _, err = resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -10,21 +10,13 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 1388: no caller composes through the controller's own composition any more. The only functions left that call it are the
|
||||
// composition's own, which issue 1389 deletes with it; this fails the moment any other function reaches for it again.
|
||||
func TestNoCallerComposesThroughTheControllersOwnComposition(t *testing.T) {
|
||||
legacy := map[string]bool{"planFor": true, "declarationWith": true, "composedAndValidated": true, "generators": true,
|
||||
"renderingFor": true, "theRestOfTheMesh": true, "whoResolves": true}
|
||||
// The composition's own pairs, until 1389: "file: caller calls callee".
|
||||
allowed := map[string]bool{
|
||||
"facts.go: composedAndValidated calls declarationWith": true,
|
||||
"facts.go: composedAndValidated calls planFor": true,
|
||||
"network.go: generators calls whoResolves": true,
|
||||
"network.go: whoResolves calls planFor": true,
|
||||
"plan.go: declarationWith calls renderingFor": true,
|
||||
"plan.go: grantsFor calls planFor": true,
|
||||
"plan.go: planFor calls theRestOfTheMesh": true,
|
||||
}
|
||||
// 1389: the controller's own composition is gone. No function of the package is named for it any more, and no function
|
||||
// calls one by that name: a name reappearing is a second copy of what mesh-sdk's go/compose holds (novox/hq ADR 0453).
|
||||
func TestTheControllersOwnCompositionIsGone(t *testing.T) {
|
||||
gone := map[string]bool{"planFor": true, "declarationWith": true, "composedAndValidated": true, "generators": true,
|
||||
"renderingFor": true, "theRestOfTheMesh": true, "whoResolves": true, "grantsFor": true, "namesInTheMesh": true,
|
||||
"onThePrivateNetwork": true, "passSeatsOn": true, "theMeshPassesSeatsOn": true, "assignmentsOf": true,
|
||||
"licencesFor": true, "seatsOn": true, "composeBusUsers": true, "adoptionFor": true, "localNetworksOf": true}
|
||||
files, err := filepath.Glob("*.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -41,12 +33,18 @@ func TestNoCallerComposesThroughTheControllersOwnComposition(t *testing.T) {
|
||||
}
|
||||
for _, d := range file.Decls {
|
||||
fn, ok := d.(*ast.FuncDecl)
|
||||
if !ok || fn.Body == nil {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if gone[fn.Name.Name] {
|
||||
found = append(found, f+": "+fn.Name.Name+" is declared")
|
||||
}
|
||||
if fn.Body == nil {
|
||||
continue
|
||||
}
|
||||
ast.Inspect(fn.Body, func(n ast.Node) bool {
|
||||
if c, ok := n.(*ast.CallExpr); ok {
|
||||
if id, ok := c.Fun.(*ast.Ident); ok && legacy[id.Name] {
|
||||
if id, ok := c.Fun.(*ast.Ident); ok && gone[id.Name] {
|
||||
found = append(found, f+": "+fn.Name.Name+" calls "+id.Name)
|
||||
}
|
||||
}
|
||||
@@ -55,9 +53,7 @@ func TestNoCallerComposesThroughTheControllersOwnComposition(t *testing.T) {
|
||||
}
|
||||
}
|
||||
sort.Strings(found)
|
||||
for _, pair := range found {
|
||||
if !allowed[pair] {
|
||||
t.Errorf("%s: it composes through the controller's own composition, not mesh-sdk's", pair)
|
||||
}
|
||||
for _, one := range found {
|
||||
t.Errorf("%s: the controller's own composition is back; compose through mesh-sdk (compose_sdk.go)", one)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
// The controller's own planning makes the credentials of files and reports.
|
||||
if _, _, err := planFor(ctx, open, "laptop"); err != nil {
|
||||
if _, _, err := resolveNode(ctx, open, "laptop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
register(t, open, aConsumerOfTheBucket("metrics"))
|
||||
|
||||
@@ -280,7 +280,7 @@ func TestAServedFactThatSaysASettingIsResolvedWithTheSettingInTheInput(t *testin
|
||||
if anchor, _ := nodeOf(in.Input, "anchor"); len(anchor.Settings["step-ca"]) == 0 {
|
||||
t.Fatalf("the Input does not carry the setting of the provider: %v", anchor.Settings)
|
||||
}
|
||||
oldPlan, _, err := planFor(ctx, open, "laptop")
|
||||
oldPlan, _, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -317,7 +317,7 @@ func TestAServedFactThatSaysAnUnsetSettingIsRefusedAsTheControllerRefusesIt(t *t
|
||||
if err := open.inventory.ClearSettings(ctx, "", "step-ca"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, oldErr := planFor(ctx, open, "laptop")
|
||||
_, _, oldErr := resolveNode(ctx, open, "laptop")
|
||||
_, newErr := readComposeInput(ctx, open)
|
||||
if (oldErr == nil) != (newErr == nil) {
|
||||
t.Fatalf("the controller's composition says %v, the adapter says %v", oldErr, newErr)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -46,26 +46,8 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co
|
||||
for _, line := range d.Passes.Said {
|
||||
fmt.Fprintln(w, line)
|
||||
}
|
||||
sp := seatPassingOn(inv, w)
|
||||
for _, p := range d.Passes.Passes {
|
||||
moved, err := inv.PassSeat(ctx, p.Seat.Name, p.Seat.Scope, p.From, p.To)
|
||||
if err != nil {
|
||||
// Said, and the record stays as it was: a composition is never refused for it.
|
||||
fmt.Fprintf(w, "%s could not be passed from %s on %s to %s on %s: %v\n", p.Seat.Name,
|
||||
p.From.Module, p.From.Node, p.To.Module, p.To.Node, err)
|
||||
continue
|
||||
}
|
||||
if !moved {
|
||||
continue // another composition passed it first, and wrote the act
|
||||
}
|
||||
if performSeatPasses(ctx, seatPassingOn(inv, w), d.Passes.Passes) {
|
||||
did = true
|
||||
if inv.Throwaway() {
|
||||
continue
|
||||
}
|
||||
if err := sp.act(ctx, p); err != nil {
|
||||
fmt.Fprintf(w, "%s passed from %s on %s to %s on %s, and the hand act could NOT be recorded: %v\n",
|
||||
p.Seat.Name, p.From.Module, p.From.Node, p.To.Module, p.To.Node, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +242,7 @@ func prepareSend(ctx context.Context, open *stores, node string) ([]string, erro
|
||||
if n.Stated && n.Contract >= compose.VaultBoxContract {
|
||||
boxes, err := coreBoxesFor(ctx, inv, node, resolved.Plan.Modules, func(m catalogue.Manifest) bool {
|
||||
return handing.Handing(m)
|
||||
}, Allocating)
|
||||
}, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestPerformAllocatesAForeseenPortAndTheNumberIsTheControllers(t *testing.T)
|
||||
t.Fatalf("a second composition names %v foreseen: %v", again.ForeseenPorts, err)
|
||||
}
|
||||
// The number is the one the controller allocates in the same store.
|
||||
plan, settings, err := planFor(ctx, open, "laptop")
|
||||
plan, settings, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -119,6 +119,24 @@ func generatorsOf(ctx context.Context, open *stores) (map[string]catalogue.Gener
|
||||
return compose.Generators(in.Input)
|
||||
}
|
||||
|
||||
// renderingOf is what a machine is rendered with as a question reads it (nothing made), with the private network: what the
|
||||
// filter's rules, the roster and the like are derived from.
|
||||
func renderingOf(ctx context.Context, open *stores, node string) (catalogue.Rendering, error) {
|
||||
in, err := inputFor(ctx, open)
|
||||
if err != nil {
|
||||
return catalogue.Rendering{}, err
|
||||
}
|
||||
rendered, err := compose.Render(in.Input, node, compose.Reading)
|
||||
if err != nil {
|
||||
return catalogue.Rendering{}, markUnresolvable(err)
|
||||
}
|
||||
with := rendered.Rendering
|
||||
if with.Generators, err = compose.Generators(in.Input); err != nil {
|
||||
return catalogue.Rendering{}, err
|
||||
}
|
||||
return with, nil
|
||||
}
|
||||
|
||||
// whoResolvesOn is the machines whose resolution answers a requirement, and why the others did not (whoResolves over the
|
||||
// Input): by what a module provides, not by its name. A machine whose set does not compose is in refused, said; one the mesh
|
||||
// could not be asked about is an error, never a refusal (novox/hq 04-ISSUES/152).
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// A declaration as the SDK composes it is the one the controller sends, byte for byte, whatever it carries: the body is the
|
||||
@@ -38,39 +37,28 @@ func TestASendableIsTheComposedDeclarationByteForByte(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1386: what a send records as carried is what the controller's own carriedOn recorded.
|
||||
// 1386: what a send records as carried: the current build of each module it carries, the build a module left out of it was last
|
||||
// sent (when that is known), and a module kept at the build the machine runs at that one. (The controller's carriedOn recorded
|
||||
// the same; mesh-sdk's node_test TestAModuleLeftOutKeepsTheBuildItWasLastSent holds the left-out case.)
|
||||
func TestWhatASendRecordsAsCarriedIsWhatTheControllerRecorded(t *testing.T) {
|
||||
plan := catalogue.Resolution{Modules: []catalogue.Manifest{{Module: "postgres"}, {Module: "files"}, {Module: "cache"}}}
|
||||
current := map[string]inventory.CurrentBuild{"postgres": {Commit: "p2"}, "files": {Commit: "f2"}, "cache": {Commit: "c2"}}
|
||||
leftOut := map[string]string{"cache": "a setting cannot compose"}
|
||||
for name, c := range map[string]struct {
|
||||
sent map[string]string
|
||||
known bool
|
||||
kept map[string]string
|
||||
want map[string]string
|
||||
}{
|
||||
"nothing sent": {},
|
||||
"sent and kept": {sent: map[string]string{"postgres": "p1", "cache": "c1", "files": "f1"}, known: true, kept: map[string]string{"postgres": "p1"}},
|
||||
"sent but not known": {sent: map[string]string{"cache": "c1"}},
|
||||
"kept a module not in": {known: true, kept: map[string]string{"mail": "m1"}},
|
||||
"nothing sent": {want: map[string]string{"postgres": "p2", "files": "f2"}},
|
||||
"sent and kept": {sent: map[string]string{"postgres": "p1", "cache": "c1", "files": "f1"}, known: true,
|
||||
kept: map[string]string{"postgres": "p1"}, want: map[string]string{"postgres": "p1", "files": "f2", "cache": "c1"}},
|
||||
"sent but not known": {sent: map[string]string{"cache": "c1"}, want: map[string]string{"postgres": "p2", "files": "f2"}},
|
||||
"kept a module not in": {known: true, kept: map[string]string{"mail": "m1"}, want: map[string]string{"postgres": "p2", "files": "f2"}},
|
||||
} {
|
||||
var names []string
|
||||
for _, m := range plan.Modules {
|
||||
names = append(names, m.Module)
|
||||
}
|
||||
before := c.sent
|
||||
if !c.known {
|
||||
before = nil
|
||||
}
|
||||
want := carriedBuilds(names, leftOut, current, before)
|
||||
for m, was := range c.kept {
|
||||
if _, carried := want[m]; carried {
|
||||
want[m] = was
|
||||
}
|
||||
}
|
||||
in := compose.Input{CurrentBuilds: map[string]string{"postgres": "p2", "files": "f2", "cache": "c2"}}
|
||||
node := compose.Node{SentBuilds: c.sent, SentKnown: c.known, KeptBuilds: c.kept}
|
||||
if got := buildsOf(in, node, plan, leftOut); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%s: a send records %v, the controller recorded %v", name, got, want)
|
||||
if got := buildsOf(in, node, plan, leftOut); !reflect.DeepEqual(got, c.want) {
|
||||
t.Errorf("%s: a send records %v, want %v", name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,21 +127,17 @@ type snapshotFacts = snapshot.Facts
|
||||
func toldEveryMachine(t *testing.T, open *stores, served map[string]map[string]any, on map[string][]string) map[string]string {
|
||||
t.Helper()
|
||||
ctx := t.Context()
|
||||
gens, err := generators(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shelf, err := open.inventory.Catalogue(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
told := map[string]string{}
|
||||
for _, node := range fourMachines {
|
||||
plan, settings, err := planFor(ctx, open, node)
|
||||
plan, _, err := resolveNode(ctx, open, node)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", node, err)
|
||||
}
|
||||
with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Allocating)
|
||||
with, err := madeAndRendered(ctx, open, node)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", node, err)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// The core's own credentials, sealed by the vault (novox/hq ADR 0349 §16, issue 755; the lead's decision on issue 755,
|
||||
@@ -40,15 +39,6 @@ import (
|
||||
// the vault and the machine, cleared by the first send the vault answers whole. Only a machine with no sealing key, or
|
||||
// one whose key cannot be read, leaves its module out.
|
||||
|
||||
// engineOpensVaultBox says whether a machine's node-engine opens the vault's box, by its own newest statement.
|
||||
func engineOpensVaultBox(ctx context.Context, inv *inventory.Inventory, node string) (bool, error) {
|
||||
stated, had, err := inv.HealthOf(ctx, node)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return had && stated.Contract >= link.VaultBoxContract, nil
|
||||
}
|
||||
|
||||
// coreAnswer is the vault's `get` answer as the controller reads it: the name, the generation and the box, never
|
||||
// opened.
|
||||
type coreAnswer struct {
|
||||
@@ -121,7 +111,7 @@ func sealingKeyPEM(raw string) (string, error) {
|
||||
// own seal where nothing was kept; on any other question, the kept box, or a stand-in the next send asks for. Pure: what to keep and what to
|
||||
// say is answered, not done.
|
||||
func coreBoxesOf(node, sealingKey string, core map[string][]string, kept map[string]map[string]inventory.CoreBox,
|
||||
choosing Choosing, ask coreAsk) coreBoxes {
|
||||
allocating bool, ask coreAsk) coreBoxes {
|
||||
out := coreBoxes{sealed: map[string]map[string]json.RawMessage{}, notPlaced: map[string]string{}}
|
||||
modules := make([]string, 0, len(core))
|
||||
for m := range core {
|
||||
@@ -143,7 +133,7 @@ func coreBoxesOf(node, sealingKey string, core map[string][]string, kept map[str
|
||||
for _, name := range core[module] {
|
||||
k, has := kept[module][name]
|
||||
valid := has && k.SealedTo == sealingKey && len(k.Box) > 0
|
||||
if choosing != Allocating {
|
||||
if !allocating {
|
||||
if valid {
|
||||
boxes[name] = k.Box
|
||||
} else {
|
||||
@@ -218,10 +208,10 @@ var coreVault = func(ctx context.Context) coreAsk {
|
||||
|
||||
// coreBoxesFor is coreBoxesOf for one machine, read from and kept in the store, and said on a send.
|
||||
func coreBoxesFor(ctx context.Context, inv *inventory.Inventory, node string, modules []catalogue.Manifest,
|
||||
handed func(catalogue.Manifest) bool, choosing Choosing) (coreBoxes, error) {
|
||||
handed func(catalogue.Manifest) bool, allocating bool) (coreBoxes, error) {
|
||||
core := coreModulesOf(modules, handed)
|
||||
if len(core) == 0 {
|
||||
if choosing == Allocating {
|
||||
if allocating {
|
||||
settleVaultSilent(ctx, node)
|
||||
}
|
||||
return coreBoxes{}, nil
|
||||
@@ -235,16 +225,16 @@ func coreBoxesFor(ctx context.Context, inv *inventory.Inventory, node string, mo
|
||||
return coreBoxes{}, err
|
||||
}
|
||||
var ask coreAsk
|
||||
if choosing == Allocating {
|
||||
if allocating {
|
||||
ask = coreVault(ctx)
|
||||
}
|
||||
out := coreBoxesOf(node, key, core, kept, choosing, ask)
|
||||
out := coreBoxesOf(node, key, core, kept, allocating, ask)
|
||||
for _, b := range out.keep {
|
||||
if err := inv.KeepCoreBox(ctx, node, b); err != nil {
|
||||
return coreBoxes{}, err
|
||||
}
|
||||
}
|
||||
if choosing == Allocating {
|
||||
if allocating {
|
||||
sayVaultSilent(ctx, node, out)
|
||||
}
|
||||
return out, nil
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestASilentVaultHoldsTheKeptBoxAndSaysSo(t *testing.T) {
|
||||
func TestACoreModuleFirstPlacedWhileTheVaultIsSilentIsSealedByTheController(t *testing.T) {
|
||||
open, vault := aCoreMesh(t, link.VaultBoxContract)
|
||||
vault.silent = errors.New("no responders")
|
||||
plan, settings, err := planFor(t.Context(), open, "laptop")
|
||||
plan, settings, err := resolveNode(t.Context(), open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func TestReplay908(t *testing.T) {
|
||||
key := "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
||||
core := map[string][]string{"nats": {"password"}}
|
||||
ask := func(name, publicKey string) (coreAnswer, error) { return coreAnswer{}, errors.New("no responders") }
|
||||
got := coreBoxesOf("novox", key, core, nil, Allocating, ask)
|
||||
got := coreBoxesOf("novox", key, core, nil, true, ask)
|
||||
if _, sealed := got.sealed["nats"]; sealed || len(got.notPlaced) != 0 {
|
||||
t.Fatalf("nats is vault-sealed %v or not placed %v", got.sealed, got.notPlaced)
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func TestReplay908(t *testing.T) {
|
||||
}
|
||||
// A kept box is still carried while the vault is silent.
|
||||
kept := map[string]map[string]inventory.CoreBox{"nats": {"password": {SealedTo: key, Box: json.RawMessage(`{"v":1}`)}}}
|
||||
got = coreBoxesOf("novox", key, core, kept, Allocating, ask)
|
||||
got = coreBoxesOf("novox", key, core, kept, true, ask)
|
||||
if string(got.sealed["nats"]["password"]) != `{"v":1}` || len(got.byController) != 0 {
|
||||
t.Fatalf("the kept box was not held: %+v", got)
|
||||
}
|
||||
@@ -99,17 +99,17 @@ func TestOnlyASendAsksTheVault(t *testing.T) {
|
||||
t.Fatal("the vault was asked by a question that is not a send")
|
||||
return coreAnswer{}, nil
|
||||
}
|
||||
for _, choosing := range []Choosing{Reading, Foreseeing} {
|
||||
if got := coreBoxesOf("anchor", key, core, kept, choosing, never); string(got.sealed["nats"]["broker"]) != `{"v":1}` {
|
||||
for _, allocating := range []bool{false} {
|
||||
if got := coreBoxesOf("anchor", key, core, kept, allocating, never); string(got.sealed["nats"]["broker"]) != `{"v":1}` {
|
||||
t.Errorf("read %s", got.sealed["nats"]["broker"])
|
||||
}
|
||||
if got := coreBoxesOf("anchor", key, core, nil, choosing, never); string(got.sealed["nats"]["broker"]) !=
|
||||
if got := coreBoxesOf("anchor", key, core, nil, allocating, never); string(got.sealed["nats"]["broker"]) !=
|
||||
string(catalogue.ForeseenVaultBox) {
|
||||
t.Errorf("with none kept, read %s", got.sealed["nats"]["broker"])
|
||||
}
|
||||
}
|
||||
// A box kept for another key of the machine is never carried.
|
||||
other := coreBoxesOf("anchor", aPublicKey(t), core, kept, Allocating,
|
||||
other := coreBoxesOf("anchor", aPublicKey(t), core, kept, true,
|
||||
func(string, string) (coreAnswer, error) { return coreAnswer{}, errors.New("silent") })
|
||||
if _, vaultSealed := other.sealed["nats"]; vaultSealed || !reflect.DeepEqual(other.byController, []string{"nats"}) {
|
||||
t.Fatalf("a box sealed to the machine's old key was carried: %+v", other)
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestAssigningAModuleThatSpeaksOnTheBusNamesItsCredential(t *testing.T) {
|
||||
}
|
||||
|
||||
// And the push refuses to send it, naming the same verb, rather than sealing a placeholder.
|
||||
plan, settings, err := planFor(ctx, open, "laptop")
|
||||
plan, settings, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -42,12 +42,8 @@ func TestAMachineIsSentTheEpochOnlyOnceItSaysItReadsOne(t *testing.T) {
|
||||
if err := inv.RecordReadsEpoch(ctx, anchor.ID, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gens, err := generators(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d := &bodiesDelivery{recordedDelivery: recordedDelivery{inv: inv}, bodies: map[string][]byte{}}
|
||||
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, composeForPush(open, gens), d, "", nil); err != nil {
|
||||
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, composeForPush(open), d, "", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
carried := func(node string) (epoch float64, has bool) {
|
||||
@@ -89,12 +85,8 @@ func TestNothingIsComposedOrSentWithoutTheLease(t *testing.T) {
|
||||
was := epochForActs
|
||||
epochForActs = func(context.Context) (uint64, error) { return 0, errors.New("this controller lost the lease") }
|
||||
t.Cleanup(func() { epochForActs = was })
|
||||
gens, err := generators(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d := &bodiesDelivery{recordedDelivery: recordedDelivery{inv: open.inventory}, bodies: map[string][]byte{}}
|
||||
refused, err := sendRound(ctx, open, []string{"anchor"}, composeForPush(open, gens), d, "", nil)
|
||||
refused, err := sendRound(ctx, open, []string{"anchor"}, composeForPush(open), d, "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/validate"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
snapshot "git.novox.be/novox/mesh-sdk/go/facts"
|
||||
@@ -504,41 +502,6 @@ func declarationFacts(ctx context.Context, open *stores, node string, gensErr er
|
||||
return d
|
||||
}
|
||||
|
||||
// composedAndValidated composes one machine's declaration — as a push would (Allocating) or as the next
|
||||
// push will without making anything (Foreseeing) — with the order it was last sent, and runs the
|
||||
// node-engine's own validator over the body. An error is that it did not compose; problems are what the
|
||||
// validator refuses.
|
||||
func composedAndValidated(ctx context.Context, open *stores, node string, gens map[string]catalogue.Generator,
|
||||
choosing Choosing) (sendable, []string, error) {
|
||||
plan, settings, err := planFor(ctx, open, node)
|
||||
if err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
declared, err := declarationWith(ctx, open, node, plan, settings, gens, choosing)
|
||||
if err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
record, err := open.inventory.NodeByName(ctx, node)
|
||||
if err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
if declared.Sequence, err = open.inventory.Sequence(ctx, record.ID); err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
if declared.Epoch, err = open.inventory.SentEpoch(ctx, record.ID); err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
// And the generation it was last sent, as the would-send is (novox/hq issue 234).
|
||||
if declared.Generation, err = open.inventory.SentGeneration(ctx, record.ID); err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
body, err := declared.Body()
|
||||
if err != nil {
|
||||
return sendable{}, nil, err
|
||||
}
|
||||
return declared, validate.Declaration(body), nil
|
||||
}
|
||||
|
||||
// resourceNames are a declaration's resources as `type:id`, sorted.
|
||||
func resourceNames(resources []map[string]any) []string {
|
||||
out := make([]string, 0, len(resources))
|
||||
|
||||
@@ -42,12 +42,8 @@ func aMeshWithSecrets(t *testing.T) (*stores, []string) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A push's worth of composition, which makes the pair credential the consumer is sent.
|
||||
gens, err := generators(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, node := range []string{"laptop", "anchor"} {
|
||||
if _, _, err := composedAndValidated(ctx, open, node, gens, Allocating); err != nil {
|
||||
if _, err := composeSend(ctx, open, node); err != nil {
|
||||
t.Fatalf("%s does not compose: %v", node, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -103,18 +102,6 @@ func senderOf(caller string) string {
|
||||
return fmt.Sprintf("%s (pid %d on %s, build %s)", caller, os.Getpid(), host, version)
|
||||
}
|
||||
|
||||
// namedModules are the modules a declaration names: its machine's set, less what was left out of it.
|
||||
func namedModules(plan catalogue.Resolution, leftOut map[string]string) []string {
|
||||
out := make([]string, 0, len(plan.Modules))
|
||||
for _, m := range plan.Modules {
|
||||
if _, left := leftOut[m.Module]; !left {
|
||||
out = append(out, m.Module)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// heardGenerationRefusal keeps a machine's refusal of a send for its generation, so S20 names its sender
|
||||
// and says it (novox/hq issue 234): on the send it refused or — when the mesh has no record of sending that
|
||||
// sequence — as a refusal of its own, naming the sender as unknown. Either way S20 is raised: a refusal the
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
|
||||
if err := inv.PinProvision(ctx, "laptop", "postgres-database", "anchor", "store"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plan, _, err := planFor(ctx, open, "laptop")
|
||||
plan, _, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
|
||||
if err := inv.RecordBindings(ctx, "laptop", boundToData(plan, nil)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
grants, _, unbound, err := grantsFor(ctx, open, "anchor")
|
||||
grants, _, unbound, err := grantsOf(ctx, open, "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
|
||||
if err := inv.PinProvision(ctx, "laptop", "postgres-database", "laptop", "store"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plan, _, err = planFor(ctx, open, "laptop")
|
||||
plan, _, err = resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
|
||||
}
|
||||
|
||||
// The store it left: no longer granted, and said.
|
||||
grants, _, unbound, err = grantsFor(ctx, open, "anchor")
|
||||
grants, _, unbound, err = grantsOf(ctx, open, "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
|
||||
strings.Join(unbound[0].BoundTo, ",") != "laptop" {
|
||||
t.Fatalf("the consumer that moved is not the one said: %+v", unbound)
|
||||
}
|
||||
planned, settings, err := planFor(ctx, open, "anchor")
|
||||
planned, settings, err := resolveNode(ctx, open, "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -105,7 +105,7 @@ func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) {
|
||||
}
|
||||
|
||||
// The store it is bound to: granted.
|
||||
grants, _, unbound, err = grantsFor(ctx, open, "laptop")
|
||||
grants, _, unbound, err = grantsOf(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -131,7 +131,7 @@ func TestAConsumerWhoseResolutionCannotBeReadIsNotWithdrawn(t *testing.T) {
|
||||
t.Fatalf("assign %s %s: %v", a[0], a[1], err)
|
||||
}
|
||||
}
|
||||
if _, _, err := planFor(ctx, open, "laptop"); err != nil {
|
||||
if _, _, err := resolveNode(ctx, open, "laptop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -144,10 +144,10 @@ func TestAConsumerWhoseResolutionCannotBeReadIsNotWithdrawn(t *testing.T) {
|
||||
if err := inv.RecordSealingKey(ctx, laptop.ID, "not a key"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := planFor(ctx, open, "laptop"); err == nil || unresolvable(err) {
|
||||
if _, _, err := resolveNode(ctx, open, "laptop"); err == nil || unresolvable(err) {
|
||||
t.Fatalf("the seam this test relies on moved: %v", err)
|
||||
}
|
||||
grants, _, unbound, err := grantsFor(ctx, open, "anchor")
|
||||
grants, _, unbound, err := grantsOf(ctx, open, "anchor")
|
||||
if err == nil {
|
||||
t.Fatalf("an unreadable consumer was answered: grants %+v, unbound %+v", grants, unbound)
|
||||
}
|
||||
|
||||
@@ -66,15 +66,11 @@ func TestTheGrantsStepSendsTheUserListAndNothingElse(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plan, settings, err := planFor(under, open, "anchor")
|
||||
plan, _, err := resolveNode(under, open, "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gens, err := generators(under, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
declared, err := declarationWith(under, open, "anchor", plan, settings, gens, Allocating)
|
||||
declared, err := composeSend(under, open, "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
@@ -265,11 +264,9 @@ func flushBehind(ctx context.Context, open *stores, nodes []inventory.Node, hand
|
||||
|
||||
// composeForPush is how a push composes one machine: its set resolved, what it cannot host and what
|
||||
// is left out of it said, and its declaration allocated.
|
||||
func composeForPush(open *stores, gens map[string]catalogue.Generator) func(held context.Context, node string) (sendable, error) {
|
||||
func composeForPush(open *stores) func(held context.Context, node string) (sendable, error) {
|
||||
return func(held context.Context, node string) (sendable, error) {
|
||||
// Read afresh for each machine: the send before it made and recorded things. gens is the private network the
|
||||
// controller's own composition took; the SDK's is computed from the same Input.
|
||||
_ = gens
|
||||
// Read afresh for each machine: the send before it made and recorded things.
|
||||
held = freshInput(held)
|
||||
plan, _, err := resolveNode(held, open, node)
|
||||
if err != nil {
|
||||
|
||||
@@ -28,11 +28,7 @@ func TestANamedPushLeavesAMachineAPolicyHoldsBack(t *testing.T) {
|
||||
if err := inv.SetUpgradeOf(ctx, "resolver", inventory.Upgrade{Why: "each machine checked by hand"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gens, err := generators(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compose := composeForPush(open, gens)
|
||||
compose := composeForPush(open)
|
||||
d := &recordedDelivery{inv: inv}
|
||||
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -134,11 +130,7 @@ func TestANamedPushStillSendsAConsequenceNothingHolds(t *testing.T) {
|
||||
if _, err := inv.Assign(ctx, "laptop", "resolver"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gens, err := generators(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compose := composeForPush(open, gens)
|
||||
compose := composeForPush(open)
|
||||
d := &recordedDelivery{inv: inv}
|
||||
if _, err := sendRound(ctx, open, []string{"anchor", "laptop"}, compose, d, "", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -99,22 +98,6 @@ func TestAPlanWaitingOnItsFirstMachineHoldsTheRest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A send records the build of each module it carried; a module left out of it keeps the build it
|
||||
// was last sent, since the machine keeps that one.
|
||||
func TestASendCarriesTheCurrentBuildsAndALeftOutModuleKeepsItsOwn(t *testing.T) {
|
||||
current := map[string]inventory.CurrentBuild{"a": {Commit: "a2"}, "b": {Commit: "b2"}, "c": {}}
|
||||
got := carriedBuilds([]string{"a", "b", "c"}, map[string]string{"b": "a setting does not compose"},
|
||||
current, map[string]string{"a": "a1", "b": "b1"})
|
||||
if want := map[string]string{"a": "a2", "b": "b1", "c": ""}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("carried %v, wanted %v", got, want)
|
||||
}
|
||||
// Not known before: the left-out module is not recorded at all, so it reads as never sent.
|
||||
got = carriedBuilds([]string{"a", "b"}, map[string]string{"b": "x"}, current, nil)
|
||||
if want := map[string]string{"a": "a2"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("carried %v, wanted %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// recordedDelivery sends nothing and records each send as the mesh does, so the next comparison
|
||||
// reads the machine as current — and writes down which machines it declared.
|
||||
type recordedDelivery struct {
|
||||
|
||||
@@ -63,7 +63,7 @@ func TestASecondEligibleHolderStandsBesideTheOneHeldByBeingAlone(t *testing.T) {
|
||||
|
||||
// And the record decides from here: the anchor's plan holds the seat, the laptop's does not.
|
||||
for node, holds := range map[string]bool{"anchor": true, "laptop": false} {
|
||||
plan, _, err := planFor(ctx, open, node)
|
||||
plan, _, err := resolveNode(ctx, open, node)
|
||||
if err != nil {
|
||||
t.Fatalf("%s no longer resolves: %v", node, err)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestAnOverflowingConsumerNeverRefusesItsProvidersMachine(t *testing.T) {
|
||||
}
|
||||
|
||||
// The consumer's machine resolves, and says which of its modules no provider will grant.
|
||||
consumer, _, err := planFor(ctx, open, "laptop")
|
||||
consumer, _, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func TestAnOverflowingConsumerNeverRefusesItsProvidersMachine(t *testing.T) {
|
||||
|
||||
// The provider's machine composes. Under ADR 0049's one bound this was a refusal naming
|
||||
// networkmanager, and no push to the provider could go through.
|
||||
plan, settings, err := planFor(ctx, open, "anchor")
|
||||
plan, settings, err := resolveNode(ctx, open, "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func TestAnOverflowingConsumerNeverRefusesItsProvidersMachine(t *testing.T) {
|
||||
if len(declared.withheld) != 1 || declared.withheld[0].Identity != "mesh_laptop_photoalbum" {
|
||||
t.Fatalf("the overflowing consumer is not the one withheld: %+v", declared.withheld)
|
||||
}
|
||||
grants, _, _, err := grantsFor(ctx, open, "anchor")
|
||||
grants, _, _, err := grantsOf(ctx, open, "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func TestReplay201(t *testing.T) {
|
||||
t.Fatalf("the send keeps the newer controller there: %v %v", keeps, err)
|
||||
}
|
||||
shelf := map[string]catalogue.Manifest{"mesh-controller": {Module: "mesh-controller", Version: "old"}}
|
||||
if _, err := keepRecorded(keepingRecorded(judged), open, "anchor", shelf); err != nil || shelf["mesh-controller"].Version != "1" {
|
||||
if err := applyKept(keepingRecorded(judged), open, "anchor", keeps, shelf); err != nil || shelf["mesh-controller"].Version != "1" {
|
||||
t.Fatalf("the send composes the kept build's manifest: %+v %v", shelf, err)
|
||||
}
|
||||
key := coreDowngradeObservation(lease.ComponentController, "anchor", "").Key()
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
"git.novox.be/novox/mesh-sdk/go/overlay"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
"github.com/novox/mesh-controller/internal/secrets"
|
||||
@@ -138,42 +140,73 @@ func TestAnIssuedValueIsADecisionInTheHandActLog(t *testing.T) {
|
||||
t.Fatal("secret issued is not in the hand-act table")
|
||||
}
|
||||
|
||||
// needOf is the need a consumer has of a provision, as mesh-sdk's resolution gives it (the controller's issuedToHolder and
|
||||
// providerMoved became the fields Needed.IssuedToHolder and ProviderMoved of compose.Resolve, issue 1389): the shelf, the
|
||||
// provider on one machine and the consumer on another.
|
||||
func needOf(t *testing.T, shelf map[string]catalogue.Manifest, provider, consumer, provision string) catalogue.Needed {
|
||||
t.Helper()
|
||||
all := map[string]catalogue.Manifest{}
|
||||
for name, m := range shelf {
|
||||
all[name] = m
|
||||
}
|
||||
all[consumer] = catalogue.Manifest{Module: consumer, Version: "1", Requires: []string{provision}}
|
||||
// The private network comes with the control plane, and both machines run it.
|
||||
var network []string
|
||||
for _, m := range provided {
|
||||
all[m.Module] = m
|
||||
if m.Module == overlay.Name {
|
||||
network = append(network, m.Module)
|
||||
}
|
||||
}
|
||||
in := compose.Input{Catalogue: all, OverlayCIDR: "10.77.0.0/16", InternalSuffix: "internal",
|
||||
Nodes: []compose.Node{
|
||||
{Name: "novox", Assigned: append([]string{provider}, network...), Place: compose.Place{Address: "10.77.0.1"}},
|
||||
{Name: "ace", Assigned: append([]string{consumer}, network...), Place: compose.Place{Address: "10.77.0.2"}}}}
|
||||
resolved, err := compose.Resolve(in, "ace")
|
||||
if err != nil {
|
||||
t.Fatalf("%s does not resolve beside %s: %v", consumer, provider, err)
|
||||
}
|
||||
for _, n := range resolved.Plan.Needs {
|
||||
if n.For == consumer && n.Name == provision {
|
||||
return n
|
||||
}
|
||||
}
|
||||
t.Fatalf("%s has no need of %s: %+v", consumer, provision, resolved.Plan.Needs)
|
||||
return catalogue.Needed{}
|
||||
}
|
||||
|
||||
// **A need's credential is the vault's for its holder alone** (issue 757) exactly when its providing module is marked
|
||||
// moved, so `secret issued` goes to the vault naming each holder, and its software issues the provision's credential.
|
||||
func TestANeedIsIssuedToItsHolderOnlyWhereTheProviderIsMovedAndItsSoftwareIssuesIt(t *testing.T) {
|
||||
forge := func(moved bool, issued bool) catalogue.Manifest {
|
||||
offer := catalogue.Offer{Name: "gitea-read"}
|
||||
offer := catalogue.Offer{Name: "gitea-read", Scope: catalogue.ScopeMesh}
|
||||
if issued {
|
||||
offer.Credential = &catalogue.OfferCredential{IssuedBy: catalogue.IssuedBySoftware, Tool: "gitea_agent_token"}
|
||||
}
|
||||
return catalogue.Manifest{Module: "gitea", Moved: moved, Provides: []catalogue.Offer{offer}}
|
||||
return catalogue.Manifest{Module: "gitea", Version: "1", Moved: moved, Provides: []catalogue.Offer{offer}}
|
||||
}
|
||||
shared := func(moved bool) catalogue.Manifest {
|
||||
return catalogue.Manifest{Module: "gitea", Moved: moved, OwnSecrets: catalogue.OwnSecrets{"key": {Path: "/k"}},
|
||||
Provides: []catalogue.Offer{{Name: "gitea-read", Credential: &catalogue.OfferCredential{Own: "key"}}}}
|
||||
return catalogue.Manifest{Module: "gitea", Version: "1", Moved: moved, OwnSecrets: catalogue.OwnSecrets{"key": {Path: "/k"}},
|
||||
Provides: []catalogue.Offer{{Name: "gitea-read", Scope: catalogue.ScopeMesh, Credential: &catalogue.OfferCredential{Own: "key"}}}}
|
||||
}
|
||||
given := func() catalogue.Manifest {
|
||||
return catalogue.Manifest{Module: "gitea", Moved: true, Provides: []catalogue.Offer{{Name: "gitea-read",
|
||||
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}}
|
||||
return catalogue.Manifest{Module: "gitea", Version: "1", Moved: true, Provides: []catalogue.Offer{{Name: "gitea-read",
|
||||
Scope: catalogue.ScopeMesh, Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}}
|
||||
}
|
||||
need := catalogue.Needed{Name: "gitea-read", From: "novox", Module: "gitea", For: "claude-code"}
|
||||
for why, c := range map[string]struct {
|
||||
shelf map[string]catalogue.Manifest
|
||||
need catalogue.Needed
|
||||
forge catalogue.Manifest
|
||||
want bool
|
||||
}{
|
||||
"moved and issued by its software": {map[string]catalogue.Manifest{"gitea": forge(true, true)}, need, true},
|
||||
"not moved": {map[string]catalogue.Manifest{"gitea": forge(false, true)}, need, false},
|
||||
"a login for each consumer": {map[string]catalogue.Manifest{"gitea": forge(true, false)}, need, true},
|
||||
"a login for each, not moved": {map[string]catalogue.Manifest{"gitea": forge(false, false)}, need, false},
|
||||
"a credential every holder shares": {map[string]catalogue.Manifest{"gitea": shared(true)}, need, false},
|
||||
"a credential a person gives": {map[string]catalogue.Manifest{"gitea": given()}, need, false},
|
||||
"a provider not on the shelf": {map[string]catalogue.Manifest{}, need, false},
|
||||
"answered by a record": {map[string]catalogue.Manifest{"gitea": forge(true, true)},
|
||||
catalogue.Needed{Name: "gitea-read", ByRecord: true, Module: "gitea", For: "claude-code"}, false},
|
||||
"moved and issued by its software": {forge(true, true), true},
|
||||
"not moved": {forge(false, true), false},
|
||||
"a login for each consumer": {forge(true, false), true},
|
||||
"a login for each, not moved": {forge(false, false), false},
|
||||
"a credential every holder shares": {shared(true), false},
|
||||
"a credential a person gives": {given(), false},
|
||||
} {
|
||||
if got := issuedToHolder(c.shelf, c.need); got != c.want {
|
||||
t.Errorf("%s: issued to its holder is %v, want %v", why, got, c.want)
|
||||
got := needOf(t, map[string]catalogue.Manifest{"gitea": c.forge}, "gitea", "claude-code", "gitea-read")
|
||||
if got.IssuedToHolder != c.want {
|
||||
t.Errorf("%s: issued to its holder is %v, want %v", why, got.IssuedToHolder, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,17 +215,14 @@ func TestANeedIsIssuedToItsHolderOnlyWhereTheProviderIsMovedAndItsSoftwareIssues
|
||||
// V3, so a need of `secret` is issued to its holder and answered by a moved provider, whether or not the module is
|
||||
// marked moved; another module is not.
|
||||
func TestTheModuleHoldingTheVaultSeatCountsAsMovedForItsConsumers(t *testing.T) {
|
||||
vault := catalogue.Manifest{Module: "mesh-vault", Claims: []catalogue.Claim{{Name: "mesh-vault"}},
|
||||
Provides: []catalogue.Offer{{Name: "secret"}}}
|
||||
other := catalogue.Manifest{Module: "mesh-vault", Provides: []catalogue.Offer{{Name: "secret"}}}
|
||||
need := catalogue.Needed{Name: "secret", Local: "admin", From: "novox", Module: "mesh-vault", For: "gitea"}
|
||||
if !issuedToHolder(map[string]catalogue.Manifest{"mesh-vault": vault}, need) ||
|
||||
!providerMoved(map[string]catalogue.Manifest{"mesh-vault": vault}, need) {
|
||||
t.Error("the vault's consumer is not answered by a moved provider")
|
||||
vault := catalogue.Manifest{Module: "mesh-vault", Version: "1", Claims: []catalogue.Claim{{Name: "mesh-vault", Scope: catalogue.ScopeMesh}},
|
||||
Provides: []catalogue.Offer{{Name: "secret", Scope: catalogue.ScopeMesh}}}
|
||||
other := catalogue.Manifest{Module: "mesh-vault", Version: "1", Provides: []catalogue.Offer{{Name: "secret", Scope: catalogue.ScopeMesh}}}
|
||||
if got := needOf(t, map[string]catalogue.Manifest{"mesh-vault": vault}, "mesh-vault", "gitea", "secret"); !got.IssuedToHolder || !got.ProviderMoved {
|
||||
t.Errorf("the vault's consumer is not answered by a moved provider: %+v", got)
|
||||
}
|
||||
if issuedToHolder(map[string]catalogue.Manifest{"mesh-vault": other}, need) ||
|
||||
providerMoved(map[string]catalogue.Manifest{"mesh-vault": other}, need) {
|
||||
t.Error("a module not holding the vault seat counts as moved")
|
||||
if got := needOf(t, map[string]catalogue.Manifest{"mesh-vault": other}, "mesh-vault", "gitea", "secret"); got.IssuedToHolder || got.ProviderMoved {
|
||||
t.Errorf("a module not holding the vault seat counts as moved: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,21 +230,21 @@ func TestTheModuleHoldingTheVaultSeatCountsAsMovedForItsConsumers(t *testing.T)
|
||||
// provides entry says `"answered-by": "helper"`, the module need not be marked moved, and no credential is the vault's
|
||||
// for it. Another provision of the module, or a module without the entry, does not count.
|
||||
func TestAnOfferAnsweredByTheHelperCountsAsMovedForThatProvisionOnly(t *testing.T) {
|
||||
proxy := catalogue.Manifest{Module: "route-proxy", Provides: []catalogue.Offer{
|
||||
{Name: "route", AnsweredBy: catalogue.AnsweredByHelper}, {Name: "other"}}}
|
||||
plain := catalogue.Manifest{Module: "route-proxy", Provides: []catalogue.Offer{{Name: "route"}}}
|
||||
proxy := catalogue.Manifest{Module: "route-proxy", Version: "1", Provides: []catalogue.Offer{
|
||||
{Name: "route", Scope: catalogue.ScopeMesh, AnsweredBy: catalogue.AnsweredByHelper}, {Name: "other", Scope: catalogue.ScopeMesh}}}
|
||||
plain := catalogue.Manifest{Module: "route-proxy", Version: "1", Provides: []catalogue.Offer{{Name: "route", Scope: catalogue.ScopeMesh}}}
|
||||
shelf := func(m catalogue.Manifest) map[string]catalogue.Manifest {
|
||||
return map[string]catalogue.Manifest{m.Module: m}
|
||||
}
|
||||
route := catalogue.Needed{Name: "route", From: "novox", Module: "route-proxy", For: "gitea"}
|
||||
other := catalogue.Needed{Name: "other", From: "novox", Module: "route-proxy", For: "gitea"}
|
||||
if !providerMoved(shelf(proxy), route) {
|
||||
if got := needOf(t, shelf(proxy), "route-proxy", "gitea", "route"); !got.ProviderMoved {
|
||||
t.Error("a provision answered by the helper is not answered by a moved provider")
|
||||
}
|
||||
if providerMoved(shelf(proxy), other) || providerMoved(shelf(plain), route) {
|
||||
t.Error("a provision not answered by the helper counts as moved")
|
||||
}
|
||||
if issuedToHolder(shelf(proxy), route) {
|
||||
} else if got.IssuedToHolder {
|
||||
t.Error("the flag made a credential the vault's")
|
||||
}
|
||||
if got := needOf(t, shelf(proxy), "route-proxy", "gitea", "other"); got.ProviderMoved {
|
||||
t.Error("a provision not answered by the helper counts as moved")
|
||||
}
|
||||
if got := needOf(t, shelf(plain), "route-proxy", "gitea", "route"); got.ProviderMoved {
|
||||
t.Error("a module without the entry counts as moved")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/overlay"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// localNetworksOf is a machine's attached networks judged (novox/hq ADR 0340): which count as its local
|
||||
// network, from what it last said of itself and the links it said face outside it. A machine that has
|
||||
// said none judges to nothing, and a port reached from `local-network` is then admitted from the private
|
||||
// network only, and said so.
|
||||
func localNetworksOf(ctx context.Context, inv *inventory.Inventory, node string) ([]catalogue.LocalNetwork, error) {
|
||||
account, had, err := inv.MachineOf(ctx, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outward, err := inv.OutwardLinksOf(ctx, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return judgedNetworks(account, had, outward, overlay.Interface), nil
|
||||
}
|
||||
|
||||
// judgedNetworks is the judging alone, from what was read.
|
||||
func judgedNetworks(account inventory.MachineAccount, had bool, outward []string, tunnel string) []catalogue.LocalNetwork {
|
||||
if !had || account.Machine == nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
@@ -55,8 +56,12 @@ func TestIssue969TheGateRaisesEachMachineAsItsEngineStatedItself(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, hands := range want {
|
||||
got, err := engineTakesHanded(ctx, raised.inventory, name)
|
||||
if err != nil || got != hands {
|
||||
in, err := readComposeInput(ctx, raised)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node, _ := nodeOf(in.Input, name)
|
||||
if got := node.Stated && node.Contract >= compose.HandedContract; got != hands {
|
||||
t.Errorf("%s: the gate raises it as taking handed files = %v (%v), the mesh says %v", name, got, err, hands)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,15 @@ func TestIssue969TheGateGivesAMachineItsContractSoItTakesHandedFiles(t *testing.
|
||||
taken := time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
stated, ok := standInHealth(snapshot.Machine{Name: "f35", Contract: link.HandedContract}, taken)
|
||||
if !ok || !takesHanded(stated, ok) {
|
||||
if !ok || stated.Contract < link.HandedContract {
|
||||
t.Fatalf("a machine whose engine takes handed files must be given that in the gate: %+v, %v", stated, ok)
|
||||
}
|
||||
old, ok := standInHealth(snapshot.Machine{Name: "e12", Contract: link.HandedContract - 1}, taken)
|
||||
if !ok || takesHanded(old, ok) {
|
||||
if !ok || old.Contract >= link.HandedContract {
|
||||
t.Fatalf("a machine whose engine is older must not take handed files: %+v, %v", old, ok)
|
||||
}
|
||||
none, ok := standInHealth(snapshot.Machine{Name: "d01"}, taken)
|
||||
if ok || takesHanded(none, ok) {
|
||||
if ok {
|
||||
t.Fatalf("a snapshot that carries no contract states nothing: %+v, %v", none, ok)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
@@ -172,11 +173,12 @@ func TestHealthIsSentOnlyToAnEngineThatSaysItReadsIt(t *testing.T) {
|
||||
inv := open.inventory
|
||||
reads := func() bool {
|
||||
t.Helper()
|
||||
got, err := engineReadsHealth(ctx, inv, "anchor")
|
||||
in, err := readComposeInput(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return got
|
||||
node, _ := nodeOf(in.Input, "anchor")
|
||||
return node.Stated && node.Contract >= compose.ReadinessContract
|
||||
}
|
||||
if reads() {
|
||||
t.Fatal("an engine that never stated anything is sent health")
|
||||
|
||||
@@ -46,15 +46,7 @@ func TestAComposedDeclarationCarriesModulesAndBuildsOnlyForANodeThatReadsThem(t
|
||||
|
||||
declare := func(node string) sendable {
|
||||
t.Helper()
|
||||
plan, settings, err := planFor(ctx, open, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gens, err := generators(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := declarationWith(ctx, open, node, plan, settings, gens, Allocating)
|
||||
d, err := composeSend(ctx, open, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/validate"
|
||||
"github.com/novox/mesh-controller/internal/chain"
|
||||
)
|
||||
|
||||
// The module of each resource and the build of each module (novox/hq issue 1145, design 47 release 1-2). The
|
||||
@@ -69,19 +68,3 @@ func TestTheNodeEnginesValidatorTakesAModulePerResourceAndABuildPerModule(t *tes
|
||||
t.Fatalf("1145/AC4: refused: %q\n%s", problems, fmt.Sprint(generic))
|
||||
}
|
||||
}
|
||||
|
||||
// The build a module carries is the one its assignment names when that is the commit carried, else the one built from
|
||||
// the commit carried; none when neither is known.
|
||||
func TestTheBuildOfACarriedCommitIsTheAssignmentsOrTheOneBuiltFromIt(t *testing.T) {
|
||||
runs := map[string]chain.Ref{"a": {Build: "b-a", Commit: "c1"}, "b": {Build: "b-old", Commit: "c0"}}
|
||||
looked := map[string]string{"b/c2": "b-new"}
|
||||
got, err := wireBuilds(map[string]string{"a": "c1", "b": "c2", "c": "c3", "d": ""}, runs,
|
||||
func(module, commit string) (string, error) { return looked[module+"/"+commit], nil })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]string{"a": "b-a", "b": "b-new"}
|
||||
if len(got) != len(want) || got["a"] != want["a"] || got["b"] != want["b"] {
|
||||
t.Fatalf("1145: builds %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,73 +316,6 @@ func graph(ctx context.Context, open *stores) ([]overlay.Node, overlay.Graph, er
|
||||
return compose.Graph(in.Input)
|
||||
}
|
||||
|
||||
// whoResolves is the machines whose resolution answers a requirement, and why the others did not.
|
||||
//
|
||||
// By what a module **provides**, not by its name. WireGuard is one way to have a private network
|
||||
// and there could be others, so a machine is on the network because something it runs provides
|
||||
// one — asking for a particular module by name would be the mistake this whole mechanism exists
|
||||
// to avoid.
|
||||
//
|
||||
// Resolved rather than read from the assignment table, because a module can arrive by being
|
||||
// required by something else, and a machine that needs the private network to do its job is on it
|
||||
// for the same reason as one that was handed it directly.
|
||||
func whoResolves(ctx context.Context, open *stores, requirement string) (
|
||||
map[string]bool, map[string]string, error) {
|
||||
inv := open.inventory
|
||||
nodes, err := inv.Nodes(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
on := map[string]bool{}
|
||||
// Why a node could not be resolved, kept rather than raised: one broken node must not stop
|
||||
// the rest being described, and whoever is rendering that node will raise it themselves.
|
||||
refused := map[string]string{}
|
||||
for _, n := range nodes {
|
||||
plan, _, err := planFor(ctx, open, n.Name)
|
||||
switch {
|
||||
case unresolvable(err):
|
||||
refused[n.Name] = err.Error()
|
||||
continue
|
||||
case err != nil:
|
||||
// Not a node that does not resolve — a question that went unanswered. Recording it as a
|
||||
// refusal would take the machine off the private network, and the generator that reads
|
||||
// this would then write a roster and a filter without it (novox/hq 04-ISSUES/152).
|
||||
return nil, nil, fmt.Errorf("whether %s answers %q cannot be read: %w",
|
||||
n.Name, requirement, err)
|
||||
}
|
||||
for _, m := range plan.Modules {
|
||||
for _, offered := range m.Offers() {
|
||||
if offered == requirement {
|
||||
on[n.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return on, refused, nil
|
||||
}
|
||||
|
||||
// rendering is everything a declaration needs, computed over the whole mesh.
|
||||
func generators(ctx context.Context, open *stores) (
|
||||
map[string]catalogue.Generator, error) {
|
||||
inv := open.inventory
|
||||
on, refused, err := whoResolves(ctx, open, overlay.Addressing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
net, err := network(ctx, inv, on, refused)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// **One generator now.** Two more used to sit beside it — the names and a resolver's zone
|
||||
// file — as modules that ran nothing. Both are facts a module asks for in its manifest
|
||||
// (`facts:` — catalogue.FactsInto), computed from the same machines this sees: the ones on the
|
||||
// private network, because a name for a machine not on it would resolve to an address nothing
|
||||
// can reach.
|
||||
return map[string]catalogue.Generator{
|
||||
overlay.Name: net,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func overlayShow(ctx context.Context, open *stores) error {
|
||||
nodes, computed, err := graph(ctx, open)
|
||||
if err != nil {
|
||||
@@ -576,68 +509,6 @@ func onTheNetwork(ctx context.Context, inv *inventory.Inventory,
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// onThePrivateNetwork is every node's address on the private network, sorted — the same set
|
||||
// the names and the resolver mean by it (onTheNetwork), so a rule saying "from the mesh" admits
|
||||
// exactly the machines the mesh names.
|
||||
//
|
||||
// A node with no address is left out rather than rendered as an empty source: an empty entry in a
|
||||
// source set is a syntax error in the rule file, and a rule file that does not load leaves the
|
||||
// node filtering whatever it was filtering before -- the one outcome worse than a wrong rule,
|
||||
// because nothing reports it.
|
||||
func onThePrivateNetwork(ctx context.Context, inv *inventory.Inventory,
|
||||
shelf map[string]catalogue.Manifest) ([]string, error) {
|
||||
places, err := onTheNetwork(ctx, inv, shelf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []string
|
||||
for _, p := range places {
|
||||
if strings.TrimSpace(p.Address) != "" {
|
||||
out = append(out, p.Address)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// namesInTheMesh is every machine's internal name and the address behind it — every machine
|
||||
// that is on the private network, the same set the resolver means by that.
|
||||
//
|
||||
// A machine that is not has no name: writing one that resolves to nothing is worse than not
|
||||
// writing it, because a connection to an address that does not answer hangs where a name that
|
||||
// does not resolve fails at once and says so. A machine placed on the overlay but not running
|
||||
// the module that puts it there is exactly that (novox/hq issue 079).
|
||||
func namesInTheMesh(ctx context.Context, inv *inventory.Inventory,
|
||||
shelf map[string]catalogue.Manifest) (map[string]string, error) {
|
||||
places, err := onTheNetwork(ctx, inv, shelf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]string{}
|
||||
for _, p := range places {
|
||||
out[overlay.InternalName(p.Name)] = p.Address
|
||||
}
|
||||
// And the carried peers the operator has named (novox/hq issue 112): machines the
|
||||
// predecessor's resolver answers for and the mesh routes to, known by name on the operator's
|
||||
// word until they enrol — at which point enrolment verifies the name and the node's own
|
||||
// entry takes over above. A name the predecessor answers for must keep resolving until the
|
||||
// machine behind it is a node; without these, taking the resolver silences three machines.
|
||||
carried, err := inv.CarriedPeers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range carried {
|
||||
if p.Named == "" || p.EnrolledAs != "" {
|
||||
continue
|
||||
}
|
||||
if _, taken := out[overlay.InternalName(p.Named)]; taken {
|
||||
continue // a node of the mesh owns the name; the stale statement loses
|
||||
}
|
||||
out[overlay.InternalName(p.Named)] = p.Address
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// artifactStoreOnNetwork is the machine on this network that offers the artifact store, and the
|
||||
// port THAT MACHINE put it on — the node's setting when it was given one (novox/hq ADR 0100,
|
||||
// 04-ISSUES/102), the mesh's assignment when it made one, and the manifest's own number only when
|
||||
|
||||
@@ -84,37 +84,6 @@ func TestAPlacementAndNothingTogetherIsRefused(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A machine is named for the others only while it is on the private network — placed AND running
|
||||
// what puts it there — the same set the resolver means by "on the private network". A machine
|
||||
// that has an address and no networking is not named: a name resolving to an address that does
|
||||
// not answer hangs where an unknown name fails at once (novox/hq issue 079).
|
||||
func TestOnlyAMachineOnThePrivateNetworkIsNamed(t *testing.T) {
|
||||
open := aMesh(t) // two placed machines, both assigned what puts them on the private network
|
||||
ctx := t.Context()
|
||||
shelf, err := open.inventory.Catalogue(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
names, err := namesInTheMesh(ctx, open.inventory, shelf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if names["anchor.internal"] != "10.77.0.1" || names["laptop.internal"] != "10.77.0.2" {
|
||||
t.Fatalf("two machines on the network are not both named: %v", names)
|
||||
}
|
||||
// The laptop keeps its place and its address, and stops running the network.
|
||||
if err := open.inventory.Unassign(ctx, "laptop", overlay.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
names, err = namesInTheMesh(ctx, open.inventory, shelf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, still := names["laptop.internal"]; still || names["anchor.internal"] != "10.77.0.1" {
|
||||
t.Fatalf("a machine that left the network is still named, or the one that stayed is not: %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0105: the range every address is composed from is the adopted tunnel's, read from
|
||||
// the tunnel the hub holds — never stored anywhere else.
|
||||
func TestTheOverlaysRangeIsTheAdoptedTunnels(t *testing.T) {
|
||||
@@ -315,16 +284,8 @@ func TestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves(t *t
|
||||
func TestTheRosterNamesOnlyTheMachines(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
gens, err := generators(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, node := range []string{"anchor", "laptop"} {
|
||||
plan, settings, err := planFor(ctx, open, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Reading)
|
||||
with, err := renderingOf(ctx, open, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
)
|
||||
|
||||
func TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer(t *testing.T) {
|
||||
@@ -23,7 +25,7 @@ func TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer(t *testing.T
|
||||
|
||||
said := answered(t, func() error { return assignCommand(ctx, "unassign", []string{"laptop", "notes"}) })
|
||||
|
||||
if _, _, err := planFor(ctx, open, "laptop"); err == nil || !unresolvable(err) {
|
||||
if _, _, err := resolveNode(ctx, open, "laptop"); err == nil || !unresolvable(err) {
|
||||
t.Fatalf("laptop resolves with two claimants of one seat: %v", err)
|
||||
}
|
||||
if n := strings.Count(said, "laptop does not resolve now"); n != 1 {
|
||||
@@ -45,7 +47,7 @@ func TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := planFor(ctx, open, "laptop"); err != nil {
|
||||
if _, _, err := resolveNode(ctx, open, "laptop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(said, "does not resolve") || strings.Contains(said, "could not be read") {
|
||||
@@ -65,7 +67,7 @@ func TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal(t *testing.T
|
||||
if err == nil {
|
||||
t.Fatalf("two claimants of one node seat resolved:\n%s", said)
|
||||
}
|
||||
_, _, planErr := planFor(ctx, open, "laptop")
|
||||
_, _, planErr := resolveNode(ctx, open, "laptop")
|
||||
if planErr == nil || planErr.Error() != err.Error() {
|
||||
t.Errorf("the act's refusal is not the plan's right after:\nact: %v\nplan: %v", err, planErr)
|
||||
}
|
||||
@@ -75,15 +77,16 @@ func TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal(t *testing.T
|
||||
}
|
||||
|
||||
// The other readers of the whole mesh still say a machine they leave out, on standard error (novox/hq
|
||||
// issue 188); an act sends it elsewhere.
|
||||
// issue 188); an act sends it elsewhere. The line is mesh-sdk's (compose.Declared.Said); where it goes is the context's.
|
||||
func TestAMachineLeftOutIsSaidWhereTheContextSays(t *testing.T) {
|
||||
line := "laptop is left out of the rest of the mesh: it does not resolve: why"
|
||||
var buf bytes.Buffer
|
||||
sayLeftOut(leftOutSaidBy(context.Background(), &buf), "laptop", errors.New("why"))
|
||||
if buf.String() != "laptop is left out of the rest of the mesh: it does not resolve: why\n" {
|
||||
sayFound(leftOutSaidBy(context.Background(), &buf), compose.Declared{Said: []string{line}})
|
||||
if buf.String() != line+"\n" {
|
||||
t.Errorf("said %q", buf.String())
|
||||
}
|
||||
got := answered(t, func() error {
|
||||
sayLeftOut(context.Background(), "laptop", errors.New("why"))
|
||||
sayFound(context.Background(), compose.Declared{Said: []string{line}})
|
||||
return nil
|
||||
})
|
||||
if !strings.Contains(got, "laptop is left out") {
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestOneAcceptForAProviderIsPlacedForEveryConsumer(t *testing.T) {
|
||||
}
|
||||
placed := func(module string) bool {
|
||||
t.Helper()
|
||||
plan, settings, err := planFor(ctx, open, "laptop")
|
||||
plan, settings, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatalf("laptop was refused: %v", err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||
)
|
||||
|
||||
// An own secret issued outside the mesh that nobody gave is never made (novox/hq issue 401, ADR 0228). Seen on
|
||||
@@ -28,7 +29,7 @@ func anAssistant() catalogue.Manifest {
|
||||
// sentTo composes what a push sends to a machine, on the send path.
|
||||
func sentTo(t *testing.T, open *stores, node string) []map[string]any {
|
||||
t.Helper()
|
||||
plan, settings, err := planFor(t.Context(), open, node)
|
||||
plan, settings, err := resolveNode(t.Context(), open, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -99,16 +100,8 @@ func TestAQuestionComposesWithoutAnOutsideSecretNobodyGave(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pushedBy(t, open, "laptop") // the mesh's own secret is made by the send
|
||||
plan, settings, err := planFor(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gens, err := generators(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, choosing := range []Choosing{Reading, Foreseeing} {
|
||||
got, err := declarationWith(ctx, open, "laptop", plan, settings, gens, choosing)
|
||||
for _, choosing := range []compose.Mode{compose.Reading, compose.Foreseeing} {
|
||||
got, _, err := composeQuestion(ctx, open, "laptop", choosing, false)
|
||||
if err != nil {
|
||||
t.Fatalf("%v: %v", choosing, err)
|
||||
}
|
||||
|
||||
@@ -63,16 +63,12 @@ func TestADeclarationIsTheSameAfterPlacementMoves(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
gens, err := generators(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, node := range []string{"anchor", "laptop"} {
|
||||
plan, settings, err := planFor(ctx, open, node)
|
||||
plan, _, err := resolveNode(ctx, open, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Allocating)
|
||||
with, err := madeAndRendered(ctx, open, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -49,15 +49,11 @@ func theWordedMesh(t *testing.T) wordedMesh {
|
||||
func toldOfTheWords(t *testing.T, open *stores, node string) string {
|
||||
t.Helper()
|
||||
ctx := t.Context()
|
||||
gens, err := generators(ctx, open)
|
||||
plan, _, err := resolveNode(ctx, open, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plan, settings, err := planFor(ctx, open, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Allocating)
|
||||
with, err := madeAndRendered(ctx, open, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+1
-1493
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"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/inventory"
|
||||
)
|
||||
@@ -21,8 +22,9 @@ func TestTheProvisionerClientsAccountIsIssuedForItsServiceAndNeverInvented(t *te
|
||||
if _, err := assign(ctx, open, "laptop", m.Module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := busCredentialIssued(ctx, inv, "laptop", m.Module, broker.ProvisionerSecret); err == nil {
|
||||
t.Fatal("a push would seal a placeholder for the client's account before it was issued")
|
||||
// The composition refuses it in the send's words (mesh-sdk's go/compose: TestTheSendsRefusalsAreRefusedInTheSendsWords).
|
||||
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 {
|
||||
@@ -38,7 +40,7 @@ func TestTheProvisionerClientsAccountIsIssuedForItsServiceAndNeverInvented(t *te
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if !runsTheProvisionerClient(m) {
|
||||
|
||||
@@ -522,8 +522,8 @@ func pushCommand(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
gens, err := generatorsOf(ctx, open)
|
||||
if err != nil {
|
||||
// The private network every declaration of the push is composed with: not computable refuses the push up front.
|
||||
if _, err := generatorsOf(ctx, open); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -724,7 +724,7 @@ func pushCommand(ctx context.Context, args []string) error {
|
||||
keep := keeping(func(held context.Context, sending []readyNode) ([]readyNode, error) {
|
||||
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...)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -48,15 +48,7 @@ func TestAPushSaysWhatItRecreates(t *testing.T) {
|
||||
}
|
||||
|
||||
// Composed as a person's push composes it.
|
||||
plan, settings, err := planFor(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)
|
||||
declared, err := composeSend(ctx, open, "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -180,16 +180,6 @@ func recordedKept(ctx context.Context, open *stores, node string) (map[string]st
|
||||
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
|
||||
// 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.
|
||||
|
||||
@@ -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.
|
||||
kept := keepingRecorded(ctx)
|
||||
plan, settings, err := planFor(kept, open, "anchor")
|
||||
plan, _, err := resolveNode(kept, open, "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -76,11 +76,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
|
||||
if imageOf(t, plan, "postgres", "server") != pgImage {
|
||||
t.Fatal("the recorded module's container lost its image")
|
||||
}
|
||||
gens, err := generators(kept, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
declared, err := declarationWith(kept, open, "anchor", plan, settings, gens, Allocating)
|
||||
declared, err := composeSend(kept, open, "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -89,7 +85,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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.
|
||||
plan, _, err = planFor(keepingRecorded(ctx, "postgres"), open, "anchor")
|
||||
plan, _, err = resolveNode(keepingRecorded(ctx, "postgres"), open, "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -128,7 +124,7 @@ func TestARecordedBuildIsCarriedOnlyByAPersonsPush(t *testing.T) {
|
||||
map[string]string{"postgres": "c0000000", "mailu": "c2222222"}); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -349,7 +349,7 @@ func TestARecordedMoveThatCannotBeJudgedWaits(t *testing.T) {
|
||||
!strings.Contains(v.said, "not in the build records") {
|
||||
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)
|
||||
}
|
||||
// laptop, judged from c1, goes; anchor waits.
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestReplay164(t *testing.T) {
|
||||
}
|
||||
sent := func() []map[string]any {
|
||||
t.Helper()
|
||||
plan, settings, err := planFor(ctx, open, "laptop")
|
||||
plan, settings, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatalf("the consumer's machine was refused for a credential only a person gives: %v", err)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func TestReplay201ThePlansSend(t *testing.T) {
|
||||
if keeping {
|
||||
c = keepingRecorded(ctx)
|
||||
}
|
||||
plan, settings, err := planFor(c, open, "anchor")
|
||||
plan, settings, err := resolveNode(c, open, "anchor")
|
||||
if err != nil {
|
||||
t.Fatalf("%s to anchor did not compose: %v", sends, err)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestReplay237(t *testing.T) {
|
||||
|
||||
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)
|
||||
}
|
||||
if !strings.Contains(said, "web on laptop now has "+catalogue.ContainerRuntimeSeat+" held") {
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestReplay401(t *testing.T) {
|
||||
}
|
||||
sent := func() []map[string]any {
|
||||
t.Helper()
|
||||
plan, settings, err := planFor(ctx, open, "laptop")
|
||||
plan, settings, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
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.
|
||||
for _, node := range []string{"laptop", "anchor"} {
|
||||
plan, settings, err := planFor(ctx, open, node)
|
||||
plan, settings, err := resolveNode(ctx, open, node)
|
||||
if err != nil {
|
||||
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"},
|
||||
[2]string{"laptop", "board"})
|
||||
|
||||
plan, _, err := planFor(ctx, open, "laptop")
|
||||
plan, _, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestASetThatDoesNotComposeIsMarkedAsTheNodesOwnProblem(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
_, _, err := planFor(t.Context(), open, "laptop")
|
||||
_, _, err := resolveNode(t.Context(), open, "laptop")
|
||||
if err == nil {
|
||||
t.Fatal("two modules claiming one seat composed anyway")
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func TestAStoreThatCannotBeReadIsNotANodeThatDoesNotCompose(t *testing.T) {
|
||||
stopped, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
_, _, err := planFor(stopped, open, "anchor")
|
||||
_, _, err := resolveNode(stopped, open, "anchor")
|
||||
if err == nil {
|
||||
t.Fatal("a plan composed against a store that could not be read")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
@@ -13,24 +12,12 @@ import (
|
||||
"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.
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
// performSeatPasses records, in the send that finds them, 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 (the composition names them: catalogue.SeatsToPassOn,
|
||||
// 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
|
||||
// record as it was: a send is never refused for it. It answers whether it recorded any.
|
||||
func performSeatPasses(ctx context.Context, sp seatPassing, passes []catalogue.SeatPass) bool {
|
||||
did := false
|
||||
for _, p := range passes {
|
||||
moved, err := sp.pass(ctx, p)
|
||||
if err != nil {
|
||||
@@ -41,21 +28,20 @@ func passSeatsOn(ctx context.Context, sp seatPassing, holdings []catalogue.Held,
|
||||
if !moved {
|
||||
continue // another composition passed it first, and wrote the act
|
||||
}
|
||||
did = true
|
||||
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",
|
||||
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,
|
||||
@@ -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
|
||||
// missing from the shelf is still assigned**: it is an assignment with no claims, so its holder is not taken for gone.
|
||||
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.
|
||||
// seatPassingOn is how a send passes a seat: against the store's record, with the hand act on the bus; on a throwaway mesh
|
||||
// (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 seatPassingOn(inv *inventory.Inventory, w io.Writer) seatPassing {
|
||||
if inv.Throwaway() {
|
||||
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{
|
||||
sp := seatPassing{
|
||||
say: w,
|
||||
pass: func(ctx context.Context, p catalogue.SeatPass) (bool, error) {
|
||||
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 {
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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 inv.Throwaway() {
|
||||
sp.act = func(context.Context, catalogue.SeatPass) error { return nil }
|
||||
}
|
||||
if assignedErr != nil {
|
||||
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))
|
||||
return sp
|
||||
}
|
||||
|
||||
@@ -26,9 +26,16 @@ func withBusSeat(t *testing.T) {
|
||||
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
|
||||
// the seat, the former holder and the new one; the holdings it answers name the new holder.
|
||||
func TestACompositionPassesTheSeatAndWritesOneHandActByTheController(t *testing.T) {
|
||||
// passesFor is what the composition names to pass on for these holdings and assignments (mesh-sdk's SeatsToPassOn), with the
|
||||
// lines it says.
|
||||
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)
|
||||
var passed []catalogue.SeatPass
|
||||
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"}}
|
||||
got := passSeatsOn(t.Context(), sp, holdings, []catalogue.Assignment{{Node: "novox", Manifest: aBrokerClaiming("gate")}})
|
||||
if len(got) != 1 || got[0].Module != "gate" || got[0].Node != "novox" {
|
||||
t.Fatalf("the holdings after the composition do not name the gate: %+v", got)
|
||||
passes, _ := passesFor(holdings, []catalogue.Assignment{{Node: "novox", Manifest: aBrokerClaiming("gate")}})
|
||||
if !performSeatPasses(t.Context(), sp, passes) {
|
||||
t.Fatal("a pass was recorded and the send does not say it made something")
|
||||
}
|
||||
if len(passed) != 1 || len(acts) != 1 {
|
||||
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.
|
||||
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.
|
||||
// A send that lost the compare-and-set writes no act, and a failed write leaves the record.
|
||||
func TestALostPassWritesNoActAndAFailedOneLeavesTheRecord(t *testing.T) {
|
||||
withBusSeat(t)
|
||||
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}
|
||||
as := []catalogue.Assignment{{Node: "novox", Manifest: aBrokerClaiming("gate")}}
|
||||
passes, _ := passesFor(holdings, as)
|
||||
lost := seatPassing{say: &bytes.Buffer{},
|
||||
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 },
|
||||
}
|
||||
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
|
||||
sp := seatPassing{say: &said,
|
||||
pass: func(context.Context, catalogue.SeatPass) (bool, error) { return false, context.DeadlineExceeded },
|
||||
act: func(context.Context, catalogue.SeatPass) error { t.Fatal("act"); return nil },
|
||||
}
|
||||
got := passSeatsOn(t.Context(), sp, holdings, as)
|
||||
if got[0].Module != "old-broker" || !strings.Contains(said.String(), "could not be passed") {
|
||||
t.Fatalf("got %+v said %q", got, said.String())
|
||||
if performSeatPasses(t.Context(), sp, passes) || !strings.Contains(said.String(), "could not be passed") {
|
||||
t.Fatalf("a failed pass is not said: %q", 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.
|
||||
func TestNoSeatIsPassedWhenAMachinesAssignmentsCouldNotBeRead(t *testing.T) {
|
||||
withBusSeat(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)
|
||||
// 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
|
||||
// it to.
|
||||
func TestAThrowawayMeshWritesNoHandActForAPass(t *testing.T) {
|
||||
inv := &inventory.Inventory{}
|
||||
inv.MarkThrowaway()
|
||||
var said bytes.Buffer
|
||||
holdings := []catalogue.Held{{Claim: "mesh-broker", Scope: catalogue.ScopeMesh, Node: "novox", Module: "old-broker"}}
|
||||
got := passSeatsOn(t.Context(), seatPassingOn(inv, &said), holdings,
|
||||
[]catalogue.Assignment{{Node: "novox", Manifest: aBrokerClaiming("gate")}})
|
||||
if len(got) != 1 || got[0].Module != "gate" {
|
||||
t.Fatalf("the gate's view does not hold the pass: %+v", got)
|
||||
sp := seatPassingOn(inv, &said)
|
||||
if err := sp.act(t.Context(), catalogue.SeatPass{}); err != nil {
|
||||
t.Fatalf("the throwaway mesh tried to write the act: %v", err)
|
||||
}
|
||||
if said.Len() != 0 {
|
||||
t.Errorf("the throwaway composition said %q (a write was tried)", said.String())
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
@@ -168,53 +165,3 @@ func (s sendable) Body() ([]byte, error) {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
plan, _, err := planFor(ctx, open, "laptop")
|
||||
plan, _, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -183,7 +183,7 @@ func TestConsumersAreToldTheGivenPort(t *testing.T) {
|
||||
!strings.Contains(err.Error(), "per node") {
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -283,7 +283,7 @@ func TestTheMachineSideOfAMappingIsMovedEverywhereTheNumberIsUsed(t *testing.T)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func helloWeb() catalogue.Manifest {
|
||||
// composed is what node would be sent now, as push composes it.
|
||||
func composed(t *testing.T, open *stores, node string) sendable {
|
||||
t.Helper()
|
||||
plan, settings, err := planFor(t.Context(), open, node)
|
||||
plan, settings, err := resolveNode(t.Context(), open, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestAModulePulledInIsHandedOnAMachineThatCanTakeItAndMirrored(t *testing.T)
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
plan, settings, err := planFor(ctx, open, "laptop")
|
||||
plan, settings, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestAProviderElsewhereServesItsOwnComposedName(t *testing.T) {
|
||||
if err := open.inventory.SetPublicDomain(ctx, "anchor", "example.test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plan, _, err := planFor(ctx, open, "laptop")
|
||||
plan, _, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func TestAProviderElsewhereServesItsOwnComposedName(t *testing.T) {
|
||||
if err := open.inventory.SetPublicDomain(ctx, "anchor", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plan, _, err = planFor(ctx, open, "laptop")
|
||||
plan, _, err = resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestPlanAndNodeListAnOptionalRequirementLeftUnbound(t *testing.T) {
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func TestPlanAndNodeListAnOptionalRequirementLeftUnbound(t *testing.T) {
|
||||
if _, err := assign(ctx, open, "anchor", "influxdb"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plan, _, err = planFor(ctx, open, "laptop")
|
||||
plan, _, err = resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
plan, settings, err := planFor(ctx, open, "laptop")
|
||||
plan, settings, err := resolveNode(ctx, open, "laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user