diff --git a/cmd/mesh-controller/addresses_test.go b/cmd/mesh-controller/addresses_test.go index dad8dce3..64cd9389 100644 --- a/cmd/mesh-controller/addresses_test.go +++ b/cmd/mesh-controller/addresses_test.go @@ -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,91 @@ 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}}}) + 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) + } + + // 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) + } +} diff --git a/cmd/mesh-controller/network.go b/cmd/mesh-controller/network.go index 283e8335..d0f2c12d 100644 --- a/cmd/mesh-controller/network.go +++ b/cmd/mesh-controller/network.go @@ -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 && len(on) == 1 { + answered, err := meshNamesAnswered(ctx, inv, shelf) + if err != nil { + return "", err + } + if !answered { + return "127.0.0.1:" + port, nil + } + } return overlay.InternalName(node) + ":" + port, nil } holder, port, found, err := artifactStoreHolder(ctx, inv) @@ -730,6 +746,17 @@ func artifactStoreAddress(ctx context.Context, inv *inventory.Inventory, return "127.0.0.1:" + port, nil } +// meshNamesAnswered is whether something answers the mesh's internal names: a holder of the mesh's +// resolver seat on the private network (novox/hq ADR 0194, ADR 0223), as a machine's resolver file +// lists them (replicatedHolders). +func meshNamesAnswered(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest) (bool, error) { + holders, err := replicatedHolders(ctx, inv, shelf) + if err != nil { + return false, err + } + return len(holders[resolverSeat]) > 0, 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) { diff --git a/testdata/store-tests b/testdata/store-tests index ef990b47..e0952edc 100644 --- a/testdata/store-tests +++ b/testdata/store-tests @@ -18,6 +18,7 @@ ./cmd/mesh-controller TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds ./internal/inventory TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid ./cmd/mesh-controller TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded +./cmd/mesh-controller TestAMeshOfOneIsHandedNoNameUntilAResolverAnswersIt ./cmd/mesh-controller TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped ./cmd/mesh-controller TestAWalkRetriedByAPersonIsSaidStartedAgain ./cmd/mesh-controller TestABacklogReleasedByAPersonIsSaidStarted