stage 3 of 8 for issue 1389
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user