Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0350b4e222 | ||
|
|
d53fea31f0 | ||
|
|
eb99284890 |
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/licences"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
"github.com/novox/mesh-controller/internal/overlay"
|
||||
)
|
||||
|
||||
// An address is read from the node's settings where it is used, never recorded with a port
|
||||
@@ -350,3 +351,159 @@ func TestOnANodeWithNoNetworkTheStoreIsReachedByLoopback(t *testing.T) {
|
||||
t.Fatalf("a builder beside the store is handed the base %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// **A mesh of one on its network, with no resolver yet, still reaches its store by loopback** (novox/hq issue
|
||||
// 701). Genesis places the network at its step 21, before anything holds the mesh's resolver seat; the
|
||||
// filter's build at step 22 was handed anchor.internal:5000, which nothing answered, and stopped. Once a
|
||||
// resolver holds the seat the store is named by the network's name; a mesh of several is answered as before
|
||||
// (TestTheRegistryTrustAndEveryImageFollowThePortTheNodeGaveTheStore).
|
||||
func TestAMeshOfOneIsHandedNoNameUntilAResolverAnswersIt(t *testing.T) {
|
||||
open := aLoneNode(t)
|
||||
ctx := t.Context()
|
||||
// On the private network, as genesis's network step leaves it: placed as the hub, able to run the
|
||||
// overlay, its key reported, the overlay assigned.
|
||||
if err := open.inventory.SetPlace(ctx, "anchor", "anchor.example:51820", "here", true, "10.77.0.1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nodes, err := open.inventory.Nodes(ctx)
|
||||
if err != nil || len(nodes) != 1 {
|
||||
t.Fatalf("nodes: %v %v", nodes, err)
|
||||
}
|
||||
if err := open.inventory.RecordProfile(ctx, nodes[0].ID, map[string]any{"capabilities": []any{
|
||||
map[string]any{"name": "container-runtime", "present": true},
|
||||
map[string]any{"name": "wireguard", "present": true},
|
||||
map[string]any{"name": "systemd", "present": true}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := open.inventory.RecordOverlayKey(ctx, nodes[0].ID, aPublicKey(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := open.inventory.Assign(ctx, "anchor", overlay.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
register(t, open, aStore())
|
||||
register(t, open, catalogue.Manifest{Module: "builder", Version: "1",
|
||||
Requires: []string{catalogue.ArtifactStoreProvision},
|
||||
Resources: []map[string]any{{"id": "server", "type": "container", "name": "mesh-builder",
|
||||
"image": "registry.example/mesh-builder@" + aDigest},
|
||||
// Where the build agent publishes, as its manifest says it.
|
||||
{"id": "env", "type": "file", "path": "/etc/mesh-builder.env",
|
||||
"content": "MESH_REGISTRY=${bound:" + catalogue.ArtifactStoreProvision + ":at}:${bound:" +
|
||||
catalogue.ArtifactStoreProvision + ":port}\n"}}})
|
||||
if err := open.inventory.RecordBuild(ctx, inventory.Build{
|
||||
ID: "b1", Repository: "r", Module: "mesh-tools", Commit: "abc",
|
||||
Made: []inventory.Artifact{{Name: "build", Kind: "image",
|
||||
Reference: catalogue.ArtifactStoreScheme + "mesh-tools/build@" + aDigest}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
register(t, open, catalogue.Manifest{Module: "mesh-tools", Version: "1"})
|
||||
for _, module := range []string{"distribution", "builder"} {
|
||||
if _, err := assign(ctx, open, "anchor", module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := open.inventory.SetSettings(ctx, "anchor", "distribution",
|
||||
map[string]any{catalogue.PortsSetting: map[string]any{"5000": 5101}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shelf, err := open.inventory.Catalogue(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
onNetwork, err := whereEveryoneIs(ctx, open.inventory, shelf)
|
||||
if err != nil || len(onNetwork) != 1 {
|
||||
t.Fatalf("the lone node is not on the network: %v %v", onNetwork, err)
|
||||
}
|
||||
if got, err := artifactStoreAddress(ctx, open.inventory, shelf, "anchor"); err != nil || got != "127.0.0.1:5101" {
|
||||
t.Fatalf("on the network with no resolver, the mesh of one is handed %q (%v), a name nothing answers", got, err)
|
||||
}
|
||||
if got := heldBy(ctx)["mesh-tools/build"]; got != "127.0.0.1:5101/mesh-tools/build@"+aDigest {
|
||||
t.Fatalf("the builder beside the store is handed the base %q", got)
|
||||
}
|
||||
publishesTo := func() string {
|
||||
t.Helper()
|
||||
for _, r := range composed(t, open, "anchor").Resources {
|
||||
if r["path"] == "/etc/mesh-builder.env" {
|
||||
content, _ := r["content"].(string)
|
||||
return content
|
||||
}
|
||||
}
|
||||
t.Fatal("the builder's environment was not composed")
|
||||
return ""
|
||||
}
|
||||
if got := publishesTo(); got != "MESH_REGISTRY=127.0.0.1:5101\n" {
|
||||
t.Fatalf("the builder beside the store publishes to %q, a name nothing answers", got)
|
||||
}
|
||||
|
||||
// A resolver holds the seat: the name is answered, and the store is named by it.
|
||||
if _, err := open.inventory.SeedSeats(ctx, catalogue.DefaultSeats()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
register(t, open, catalogue.Manifest{Module: "resolver", Version: "1",
|
||||
Provides: []catalogue.Offer{{Name: "wildcard-resolution", Scope: catalogue.ScopeMesh}},
|
||||
Claims: []catalogue.Claim{{Name: "mesh-dns-resolver", Scope: catalogue.ScopeMesh}}})
|
||||
if _, err := assign(ctx, open, "anchor", "resolver"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shelf, err = open.inventory.Catalogue(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, err := artifactStoreAddress(ctx, open.inventory, shelf, "anchor"); err != nil || got != "anchor.internal:5101" {
|
||||
t.Fatalf("with a resolver answering the mesh's names, the store is handed as %q (%v)", got, err)
|
||||
}
|
||||
if got := heldBy(ctx)["mesh-tools/build"]; got != "anchor.internal:5101/mesh-tools/build@"+aDigest {
|
||||
t.Fatalf("the builder is handed the base %q", got)
|
||||
}
|
||||
if got := publishesTo(); got != "MESH_REGISTRY=anchor.internal:5101\n" {
|
||||
t.Fatalf("with a resolver, the builder publishes to %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// **A mesh of two in which one machine does not resolve is still a mesh of two** (review 275). The reading of
|
||||
// who is on the network leaves the unresolvable laptop out, which made it look like a mesh of one; the
|
||||
// store's own node must keep its internal name, and its builder keep publishing to it.
|
||||
func TestAMeshOfTwoWithAnUnresolvableMachineKeepsItsNames(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
register(t, open, aStore())
|
||||
register(t, open, catalogue.Manifest{Module: "builder", Version: "1",
|
||||
Requires: []string{catalogue.ArtifactStoreProvision},
|
||||
Resources: []map[string]any{{"id": "env", "type": "file", "path": "/etc/mesh-builder.env",
|
||||
"content": "MESH_REGISTRY=${bound:" + catalogue.ArtifactStoreProvision + ":at}:${bound:" +
|
||||
catalogue.ArtifactStoreProvision + ":port}\n"}}})
|
||||
for _, module := range []string{"distribution", "builder"} {
|
||||
if _, err := assign(ctx, open, "anchor", module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := open.inventory.SetSettings(ctx, "anchor", "distribution",
|
||||
map[string]any{catalogue.PortsSetting: map[string]any{"5000": 5101}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The laptop does not resolve: two modules claim one node seat there, straight into the store.
|
||||
twoRivalsAndANote(t, open)
|
||||
for _, m := range []string{"rival-one", "rival-two"} {
|
||||
if _, err := open.inventory.Assign(ctx, "laptop", m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
shelf, err := open.inventory.Catalogue(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
onNetwork, err := whereEveryoneIs(ctx, open.inventory, shelf)
|
||||
if err != nil || len(onNetwork) != 1 {
|
||||
t.Fatalf("the unresolvable laptop is still read as on the network: %v %v", onNetwork, err)
|
||||
}
|
||||
|
||||
if got, err := artifactStoreAddress(ctx, open.inventory, shelf, "anchor"); err != nil || got != "anchor.internal:5101" {
|
||||
t.Fatalf("a mesh of two read as one: the store's own node is handed %q (%v)", got, err)
|
||||
}
|
||||
for _, r := range composed(t, open, "anchor").Resources {
|
||||
if r["path"] == "/etc/mesh-builder.env" && r["content"] != "MESH_REGISTRY=anchor.internal:5101\n" {
|
||||
t.Fatalf("a mesh of two read as one: the builder publishes to %v", r["content"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,6 +721,22 @@ func artifactStoreAddress(ctx context.Context, inv *inventory.Inventory,
|
||||
return "", err
|
||||
}
|
||||
if found {
|
||||
// **The store's own node is not handed a name nothing answers** (novox/hq issue 701). A node's
|
||||
// internal name is answered by the mesh's resolver (ADR 0194), and genesis places the network
|
||||
// (its step 21) before anything holds that seat: the filter's build was handed
|
||||
// anchor.internal:5000 and the machine's runtime could not look it up. Until a resolver holds
|
||||
// the seat, a mesh of one keeps reaching its store by loopback, as it did before the network.
|
||||
// A mesh of more than one node is answered as before: loopback is no answer for the others, and
|
||||
// one name for every node is what a mesh of several without a resolver is still put together to.
|
||||
if node == forNode {
|
||||
unanswered, err := aLoneNameNothingAnswers(ctx, inv)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if unanswered {
|
||||
return "127.0.0.1:" + port, nil
|
||||
}
|
||||
}
|
||||
return overlay.InternalName(node) + ":" + port, nil
|
||||
}
|
||||
holder, port, found, err := artifactStoreHolder(ctx, inv)
|
||||
@@ -730,6 +746,50 @@ func artifactStoreAddress(ctx context.Context, inv *inventory.Inventory,
|
||||
return "127.0.0.1:" + port, nil
|
||||
}
|
||||
|
||||
// aLoneNameNothingAnswers is whether the mesh is one node and nothing will answer that node's internal
|
||||
// name: the inventory knows exactly one node, and no module holds or claims the mesh's resolver seat
|
||||
// anywhere (novox/hq ADR 0194, ADR 0223). Genesis is exactly that from its network step until a resolver is
|
||||
// installed (novox/hq issue 701): the node then reaches what runs on it by loopback, as it did before the
|
||||
// network, and is not composed a name no lookup can answer.
|
||||
//
|
||||
// **Read from the mesh, never from who is on the network** (review 275): that reading leaves out a machine
|
||||
// whose assignments cannot be read or whose modules do not resolve, and a bad reading must never make a
|
||||
// mesh of several look like a mesh of one and send its machines to loopback. Every recorded holding and
|
||||
// every assignment of a claimant counts, wherever it is and whether or not its machine has an address.
|
||||
func aLoneNameNothingAnswers(ctx context.Context, inv *inventory.Inventory) (bool, error) {
|
||||
nodes, err := inv.Nodes(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(nodes) != 1 {
|
||||
return false, nil
|
||||
}
|
||||
holdings, err := inv.Holdings(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, h := range holdings {
|
||||
if s, ok := catalogue.SeatNamed(h.Claim); ok && s.Name == resolverSeat {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
entries, err := inv.Catalogued(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if len(e.On) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, c := range e.Manifest.Claims {
|
||||
if s, ok := catalogue.SeatNamed(c.Name); ok && s.Name == resolverSeat {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// artifactStoreHolder is whichever node is assigned a module offering the artifact store, on or
|
||||
// off the network, and the port that node put it on.
|
||||
func artifactStoreHolder(ctx context.Context, inv *inventory.Inventory) (node, port string, found bool, err error) {
|
||||
|
||||
@@ -103,6 +103,19 @@ func planFor(ctx context.Context, open *stores, nodeName string) (catalogue.Reso
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
// **This node's own name, only where something answers it** (novox/hq issue 701). A mesh of one on its
|
||||
// network with no resolver yet is composed as off it: what it binds to on itself is reached by
|
||||
// loopback, as before the network, rather than by a name no lookup on the machine can answer.
|
||||
ownName := onNetwork[nodeName]
|
||||
if ownName != "" {
|
||||
unanswered, err := aLoneNameNothingAnswers(ctx, inv)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
if unanswered {
|
||||
ownName = ""
|
||||
}
|
||||
}
|
||||
|
||||
// What this mesh can answer with a record rather than a machine, and which record each of
|
||||
// this node's modules was put on. Read across a context boundary by name, which is what
|
||||
@@ -136,7 +149,7 @@ func planFor(ctx context.Context, open *stores, nodeName string) (catalogue.Reso
|
||||
|
||||
resolved, err := catalogue.Resolve(shelf, assigned,
|
||||
catalogue.Node{Name: nodeName, Site: site, Capabilities: capabilities,
|
||||
At: onNetwork[nodeName], PublicDomain: publicDomain,
|
||||
At: ownName, PublicDomain: publicDomain,
|
||||
Account: who.Account, AccountHome: who.AccountHome,
|
||||
AgentAccount: who.AgentAccount, AgentAccountHome: who.AgentAccountHome}, world)
|
||||
if err != nil {
|
||||
|
||||
Vendored
+2
@@ -18,6 +18,8 @@
|
||||
./cmd/mesh-controller TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds
|
||||
./internal/inventory TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid
|
||||
./cmd/mesh-controller TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded
|
||||
./cmd/mesh-controller TestAMeshOfOneIsHandedNoNameUntilAResolverAnswersIt
|
||||
./cmd/mesh-controller TestAMeshOfTwoWithAnUnresolvableMachineKeepsItsNames
|
||||
./cmd/mesh-controller TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped
|
||||
./cmd/mesh-controller TestAWalkRetriedByAPersonIsSaidStartedAgain
|
||||
./cmd/mesh-controller TestABacklogReleasedByAPersonIsSaidStarted
|
||||
|
||||
Reference in New Issue
Block a user