diff --git a/cmd/mesh-controller/addresses_test.go b/cmd/mesh-controller/addresses_test.go index 64cd9389..a4025239 100644 --- a/cmd/mesh-controller/addresses_test.go +++ b/cmd/mesh-controller/addresses_test.go @@ -385,7 +385,11 @@ func TestAMeshOfOneIsHandedNoNameUntilAResolverAnswersIt(t *testing.T) { 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}}}) + "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", @@ -417,6 +421,20 @@ func TestAMeshOfOneIsHandedNoNameUntilAResolverAnswersIt(t *testing.T) { 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 { @@ -438,4 +456,7 @@ func TestAMeshOfOneIsHandedNoNameUntilAResolverAnswersIt(t *testing.T) { 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) + } } diff --git a/cmd/mesh-controller/network.go b/cmd/mesh-controller/network.go index d0f2c12d..51944230 100644 --- a/cmd/mesh-controller/network.go +++ b/cmd/mesh-controller/network.go @@ -728,12 +728,12 @@ func artifactStoreAddress(ctx context.Context, inv *inventory.Inventory, // 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 node == forNode { + unanswered, err := aLoneNameNothingAnswers(ctx, inv, shelf, len(on)) if err != nil { return "", err } - if !answered { + if unanswered { return "127.0.0.1:" + port, nil } } @@ -746,15 +746,21 @@ 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) { +// aLoneNameNothingAnswers is whether the mesh is one node on its private network and nothing answers that +// node's internal name: no holder of the mesh's resolver seat (novox/hq ADR 0194, ADR 0223), as a machine's +// resolver file lists them (replicatedHolders). 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. +func aLoneNameNothingAnswers(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest, + onNetwork int) (bool, error) { + if onNetwork != 1 { + return false, nil + } holders, err := replicatedHolders(ctx, inv, shelf) if err != nil { return false, err } - return len(holders[resolverSeat]) > 0, nil + return len(holders[resolverSeat]) == 0, nil } // artifactStoreHolder is whichever node is assigned a module offering the artifact store, on or diff --git a/cmd/mesh-controller/plan.go b/cmd/mesh-controller/plan.go index acfb086d..3d11f00d 100644 --- a/cmd/mesh-controller/plan.go +++ b/cmd/mesh-controller/plan.go @@ -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, shelf, len(onNetwork)) + 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 {