From c4782ae2fd24e401761cb515461da735177542d3 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 30 Aug 2026 00:02:18 +0200 Subject: [PATCH] An app is told where its database is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Knowing that a machine needs the anchor's database is useless to the program that needs it unless the program is told. It knew; nothing was written anywhere it could read. Two fields, mirroring contributes/receives in the other direction: serves: {database: {port: 5432, driver: postgres}} on the provider binds: {database: /etc/app/database.json} on the consumer The provider says what a consumer needs to know; the mesh adds the half only it has — which machine, and what that machine is called on the private network. The file says, in itself, that it carries no credential and why. A missing field looks like a bug; a stated absence looks like a boundary. Binding something answered on this machine writes nothing. A file saying "it is on this node" is a fact nobody needs and one more thing to keep true. And two machines that share no private network are refused rather than wired together. An app here and a database there with no path between them is a mesh that reports itself configured and does not work — the failure surfaces as a connection timing out, which is the slowest place to find it. This is checkable now only because the network became something a machine is given rather than something it has by having an address. One fault, found by running it: working out who is on the private network resolved the mesh, and resolving the mesh asks who is on the private network. It hung for two minutes. The comment above the function said not to do that and the function did it anyway; it now resolves each node locally, which is the right answer to the question regardless — whether a machine is on the network depends on what it was assigned, not on what it takes from others. --- cmd/mesh-control/main.go | 88 +++++++++++++- internal/catalogue/brokered_test.go | 173 ++++++++++++++++++++++++++-- internal/catalogue/manifest.go | 49 ++++++++ internal/catalogue/resolve.go | 115 +++++++++++++++--- internal/catalogue/settings.go | 6 + 5 files changed, 399 insertions(+), 32 deletions(-) diff --git a/cmd/mesh-control/main.go b/cmd/mesh-control/main.go index 6979387..abc8f49 100644 --- a/cmd/mesh-control/main.go +++ b/cmd/mesh-control/main.go @@ -994,8 +994,13 @@ func planFor(ctx context.Context, inv *inventory.Inventory, nodeName string) (ca return catalogue.Resolution{}, nil, err } + onNetwork, err := whereEveryoneIs(ctx, inv, shelf) + if err != nil { + return catalogue.Resolution{}, nil, err + } resolved, err := catalogue.Resolve(shelf, assigned, - catalogue.Node{Name: nodeName, Site: site, Capabilities: capabilities}, world) + catalogue.Node{Name: nodeName, Site: site, Capabilities: capabilities, + At: onNetwork[nodeName]}, world) if err != nil { return catalogue.Resolution{}, nil, err } @@ -1053,6 +1058,13 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory, for _, p := range places { siteOf[p.Name] = p.Site } + // Which machines are actually on the private network, and what they are called there. Not + // "has an address" — that was true of every placed machine and told you nothing about whether + // anything could reach it. It is what resolved the module. + onNetwork, err := whereEveryoneIs(ctx, inv, shelf) + if err != nil { + return catalogue.World{}, err + } type candidate struct { node catalogue.Node @@ -1069,10 +1081,11 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory, } caps, _ := inv.ProfileOf(ctx, n.Name) others = append(others, candidate{ - catalogue.Node{Name: n.Name, Site: siteOf[n.Name], Capabilities: caps}, theirs}) + catalogue.Node{Name: n.Name, Site: siteOf[n.Name], Capabilities: caps, + At: onNetwork[n.Name]}, theirs}) } - offered := map[string][]string{} + offered := map[string][]catalogue.Provider{} for _, o := range others { got, err := catalogue.Resolve(shelf, o.assigned, o.node, catalogue.World{Unchecked: true}) if err != nil { @@ -1082,12 +1095,29 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory, } for _, m := range got.Modules { for _, name := range m.OffersAt(catalogue.ScopeMesh) { - offered[name] = append(offered[name], o.node.Name) + // What that module says a consumer needs to know, with that node's settings on + // it: a port somebody moved on the provider is a port its consumers must be told + // about, and the two coming from different places is how they come to disagree. + serves := m.Serves[name] + if len(serves) > 0 { + layers, err := inv.SettingsFor(ctx, o.node.Name, m.Module) + if err != nil { + return catalogue.World{}, err + } + serves, err = catalogue.Settle(serves, layers) + if err != nil { + return catalogue.World{}, err + } + } + offered[name] = append(offered[name], catalogue.Provider{ + Node: o.node.Name, At: o.node.At, Serves: serves}) } } } for k := range offered { - sort.Strings(offered[k]) + sort.Slice(offered[k], func(i, j int) bool { + return offered[k][i].Node < offered[k][j].Node + }) } world := catalogue.World{Offered: offered} @@ -1101,6 +1131,54 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory, return world, nil } +// whereEveryoneIs is each machine's name on the private network, for the ones on it. +// +// **Resolved without consulting the rest of the mesh**, and that is not an optimisation. Every +// other path here answers a question about one node by resolving the others; this one is called +// *from* that path, so doing the same would not terminate — which it did not, for two minutes, +// until it was run. +// +// An unchecked resolution is exactly right for the question anyway. Whether a machine is on the +// private network depends on what it was assigned and what that requires, both of which are local +// facts. What it takes *from* other machines does not change the answer. +// +// The distinction that matters is kept: a machine absent from the network module's own view is +// absent here, so "has an address" is not mistaken for "is reachable" — which it was, before the +// network became something a machine is given. +func whereEveryoneIs(ctx context.Context, inv *inventory.Inventory, + shelf map[string]catalogue.Manifest) (map[string]string, error) { + + places, err := inv.Overlays(ctx) + if err != nil { + return nil, err + } + out := map[string]string{} + for _, p := range places { + if p.Address == "" { + continue + } + assigned, err := inv.Assigned(ctx, p.Name) + if err != nil || len(assigned) == 0 { + continue + } + caps, _ := inv.ProfileOf(ctx, p.Name) + got, err := catalogue.Resolve(shelf, assigned, + catalogue.Node{Name: p.Name, Site: p.Site, Capabilities: caps}, + catalogue.World{Unchecked: true}) + if err != nil { + continue + } + for _, m := range got.Modules { + for _, offered := range m.Offers() { + if offered == overlay.Requirement { + out[p.Name] = overlay.InternalName(p.Name) + } + } + } + } + return out, nil +} + func planCommand(ctx context.Context, args []string) error { set := flag.NewFlagSet("plan", flag.ContinueOnError) // Because "one resource" does not tell you whether the settings landed. Being able to read diff --git a/internal/catalogue/brokered_test.go b/internal/catalogue/brokered_test.go index 096096a..c404992 100644 --- a/internal/catalogue/brokered_test.go +++ b/internal/catalogue/brokered_test.go @@ -1,6 +1,7 @@ package catalogue import ( + "encoding/json" "strings" "testing" ) @@ -12,6 +13,23 @@ import ( // `requires`, so both were answered the same way, and the second answer was to install PostgreSQL // on every machine that runs a web application. +// onNetwork is a set of providers, all of them reachable. Written as a helper because a machine +// that cannot reach the one answering its requirement is its own case, tested separately. +// reachable is this machine, on the private network. +func reachable() Node { + n := workstation() + n.At = "workstation.internal" + return n +} + +func onNetwork(nodes ...string) map[string][]Provider { + out := make([]Provider, 0, len(nodes)) + for _, n := range nodes { + out = append(out, Provider{Node: n, At: n + ".internal"}) + } + return map[string][]Provider{"database": out} +} + func brokeredShelf() map[string]Manifest { return shelf( Manifest{Module: "postgres", Version: "1", Provides: FromAnywhere("database")}, @@ -21,8 +39,8 @@ func brokeredShelf() map[string]Manifest { func TestADatabaseIsNotInstalledOnEveryMachineThatUsesOne(t *testing.T) { // The fault this whole distinction exists for. - got, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), - World{Offered: map[string][]string{"database": {"anchor"}}}) + got, err := Resolve(brokeredShelf(), []string{"meshboard"}, reachable(), + World{Offered: onNetwork("anchor")}) if err != nil { t.Fatal(err) } @@ -34,8 +52,8 @@ func TestADatabaseIsNotInstalledOnEveryMachineThatUsesOne(t *testing.T) { func TestWhatAMachineTakesFromElsewhereIsRecorded(t *testing.T) { // It is the only part of a node's set that stops working when a *different* machine goes // away, and it is where a credential will have to be handed back. - got, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), - World{Offered: map[string][]string{"database": {"anchor"}}}) + got, err := Resolve(brokeredShelf(), []string{"meshboard"}, reachable(), + World{Offered: onNetwork("anchor")}) if err != nil { t.Fatal(err) } @@ -65,8 +83,8 @@ func TestNothingInTheMeshProvidingItIsRefusedWithSomewhereToPutIt(t *testing.T) func TestTwoNodesProvidingItIsRefusedRatherThanPicked(t *testing.T) { // Same rule as everywhere else. Picking one would be a guess about which database a person // meant, and the wrong guess is somebody's data in the wrong place. - _, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), - World{Offered: map[string][]string{"database": {"anchor", "archive"}}}) + _, err := Resolve(brokeredShelf(), []string{"meshboard"}, reachable(), + World{Offered: onNetwork("anchor", "archive")}) if err == nil { t.Fatal("one of two databases was picked silently") } @@ -78,9 +96,9 @@ func TestTwoNodesProvidingItIsRefusedRatherThanPicked(t *testing.T) { } func TestSayingWhichOneSettlesIt(t *testing.T) { - got, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), + got, err := Resolve(brokeredShelf(), []string{"meshboard"}, reachable(), World{ - Offered: map[string][]string{"database": {"anchor", "archive"}}, + Offered: onNetwork("anchor", "archive"), Pinned: map[string]string{"database": "archive"}, }) if err != nil { @@ -94,9 +112,9 @@ func TestSayingWhichOneSettlesIt(t *testing.T) { func TestBeingPointedAtAMachineThatDoesNotProvideItIsRefused(t *testing.T) { // Rather than falling back to one that does. A fallback would quietly move somebody's data to // a machine they did not choose, which is the whole reason the question is asked. - _, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), + _, err := Resolve(brokeredShelf(), []string{"meshboard"}, reachable(), World{ - Offered: map[string][]string{"database": {"anchor", "archive"}}, + Offered: onNetwork("anchor", "archive"), Pinned: map[string]string{"database": "somewhere-else"}, }) if err == nil { @@ -110,9 +128,9 @@ func TestBeingPointedAtAMachineThatDoesNotProvideItIsRefused(t *testing.T) { func TestOneProviderDoesNotOverruleAChoice(t *testing.T) { // A single answer is normally taken silently. Not when somebody said they wanted a different // one -- that is the mesh overruling a person, which it does nowhere else. - _, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), + _, err := Resolve(brokeredShelf(), []string{"meshboard"}, reachable(), World{ - Offered: map[string][]string{"database": {"anchor"}}, + Offered: onNetwork("anchor"), Pinned: map[string]string{"database": "archive"}, }) if err == nil { @@ -177,3 +195,134 @@ func TestASiteScopedProvisionIsRefused(t *testing.T) { t.Fatalf("unhelpful refusal: %v", err) } } + +// Being told, which is the difference between knowing and being able to. + +func boundShelf() map[string]Manifest { + return shelf( + Manifest{Module: "postgres", Version: "1", Provides: FromAnywhere("database"), + Serves: map[string]map[string]any{"database": {"port": 5432, "driver": "postgres"}}}, + Manifest{Module: "meshboard", Version: "1", Requires: []string{"database"}, + Binds: map[string]string{"database": "/etc/meshboard/database.json"}}, + ) +} + +func binding(t *testing.T, out []map[string]any) map[string]any { + t.Helper() + for _, r := range out { + if r["path"] != "/etc/meshboard/database.json" { + continue + } + var parsed map[string]any + if err := json.Unmarshal([]byte(r["content"].(string)), &parsed); err != nil { + t.Fatalf("what the app is told is not readable: %v", err) + } + return parsed + } + t.Fatalf("the app was told nothing: %v", out) + return nil +} + +func TestAnAppIsToldWhereItsDatabaseIs(t *testing.T) { + // Knowing it needs the anchor's database is useless to the program that needs it unless the + // program is told. This is the whole point of the field. + got, err := Resolve(boundShelf(), []string{"meshboard"}, reachable(), + World{Offered: map[string][]Provider{"database": {{Node: "anchor", At: "anchor.internal", + Serves: map[string]any{"port": 5432, "driver": "postgres"}}}}}) + if err != nil { + t.Fatal(err) + } + told := binding(t, mustDeclare(t, got)) + if told["from"] != "anchor" || told["at"] != "anchor.internal" { + t.Fatalf("it was not told where: %v", told) + } + serves, _ := told["serves"].(map[string]any) + if serves["port"] != float64(5432) || serves["driver"] != "postgres" { + t.Fatalf("it was not told how: %v", serves) + } +} + +func TestItSaysItCarriesNoCredential(t *testing.T) { + // A missing field looks like a bug; a stated absence looks like a boundary. Somebody wiring + // this up must not spend an afternoon looking for the password field. + got, _ := Resolve(boundShelf(), []string{"meshboard"}, reachable(), + World{Offered: map[string][]Provider{"database": {{Node: "anchor", At: "anchor.internal"}}}}) + told := binding(t, mustDeclare(t, got)) + note, _ := told["generated"].(string) + if !strings.Contains(note, "no credential") { + t.Fatalf("the file does not say what it does not carry: %v", note) + } + for key := range told { + if strings.Contains(key, "password") || strings.Contains(key, "secret") { + t.Fatalf("something that looks like a credential appeared: %q", key) + } + } +} + +func TestTwoMachinesThatCannotReachEachOtherAreRefused(t *testing.T) { + // An app on one machine and a database on another that share no private network is a mesh + // that reports itself configured and does not work. Said here rather than discovered as a + // connection timing out. + _, err := Resolve(boundShelf(), []string{"meshboard"}, workstation(), // not on the network + World{Offered: map[string][]Provider{"database": {{Node: "anchor", At: "anchor.internal"}}}}) + if err == nil { + t.Fatal("an app was pointed at a database it has no path to") + } + if !strings.Contains(err.Error(), "private network") { + t.Fatalf("the refusal does not say what is wrong: %v", err) + } + if !strings.Contains(err.Error(), meshNetwork) { + t.Fatalf("the refusal does not say what to assign: %v", err) + } +} + +func TestTheProviderBeingOffTheNetworkIsAlsoRefused(t *testing.T) { + // Both directions, because the failure is identical from either end and the remedy differs. + _, err := Resolve(boundShelf(), []string{"meshboard"}, reachable(), + World{Offered: map[string][]Provider{"database": {{Node: "anchor"}}}}) + if err == nil { + t.Fatal("an app was pointed at a database that is not on the private network") + } + if !strings.Contains(err.Error(), "anchor") { + t.Fatalf("the refusal does not name the unreachable end: %v", err) + } +} + +func TestBindingSomethingAnsweredHereWritesNothing(t *testing.T) { + // A file saying "it is on this node" is a fact nobody needs and one more thing to keep true. + got, err := Resolve(shelf( + Manifest{Module: "zsh", Version: "1", Provides: Offers("shell")}, + Manifest{Module: "tools", Version: "1", Requires: []string{"shell"}, + Binds: map[string]string{"shell": "/etc/tools/shell.json"}}, + ), []string{"tools"}, reachable(), World{}) + if err != nil { + t.Fatal(err) + } + for _, r := range mustDeclare(t, got) { + if r["path"] == "/etc/tools/shell.json" { + t.Fatalf("a binding was written for something on this machine: %v", r) + } + } +} + +func TestBindingWhatYouDoNotRequireIsRefused(t *testing.T) { + _, err := ParseManifest([]byte(`{"module":"app","version":"1", + "binds":{"database":"/etc/app/db.json"}}`)) + if err == nil { + t.Fatal("a module was told about something it never asked for") + } + if !strings.Contains(err.Error(), "does not require") { + t.Fatalf("unhelpful refusal: %v", err) + } +} + +func TestServingWhatYouDoNotProvideIsRefused(t *testing.T) { + _, err := ParseManifest([]byte(`{"module":"app","version":"1", + "serves":{"database":{"port":5432}}}`)) + if err == nil { + t.Fatal("a module served something it does not provide") + } + if !strings.Contains(err.Error(), "does not provide") { + t.Fatalf("unhelpful refusal: %v", err) + } +} diff --git a/internal/catalogue/manifest.go b/internal/catalogue/manifest.go index d42b650..b7b6e91 100644 --- a/internal/catalogue/manifest.go +++ b/internal/catalogue/manifest.go @@ -162,8 +162,27 @@ type Manifest struct { // delivers the facts, and the module turns them into whatever it runs. That boundary is why // swapping the proxy does not touch a single module that publishes through it. Receives map[string]string `json:"receives,omitempty"` + + // Serves is what a consumer needs to know in order to use something this module provides — a + // port, a path, a realm. The module's half of the answer; the mesh adds the other half, which + // is *which machine* and *where it is on the private network*. + // + // It does not carry a credential and cannot: a manifest is the same on every mesh, and a + // secret is the one thing that must not be. + Serves map[string]map[string]any `json:"serves,omitempty"` + + // Binds is where this module wants to be told about something it requires, per requirement. + // + // Because "this machine needs a database from the anchor" is useless to the program that + // needs it unless the program is told. A file, like everything else — the host writes files + // and knows nothing about provisions, which is what keeps this from needing anything new + // down there. + Binds map[string]string `json:"binds,omitempty"` } +// BoundID is the resource identity of the file a module is told about a provision in. +func BoundID(requirement string) string { return "bound-" + requirement } + // Wants is everything that must be provided on the same node: what this module requires, and what // it contributes to. func (m Manifest) Wants() []string { @@ -263,6 +282,36 @@ func ParseManifest(raw []byte) (Manifest, error) { "%s contributes nothing to %q; if it only needs one, require it", m.Module, to)) } } + for to := range m.Serves { + var offered bool + for _, o := range m.Offers() { + if o == to { + offered = true + } + } + if !offered { + problems = append(problems, fmt.Sprintf( + "%s serves %q to whoever requires it, and does not provide it", m.Module, to)) + } + } + for to, where := range m.Binds { + if !strings.HasPrefix(where, "/") { + problems = append(problems, fmt.Sprintf( + "%s binds %q at %q, which is not an absolute path", m.Module, to, where)) + } + var wanted bool + for _, w := range m.Wants() { + if w == to { + wanted = true + } + } + if !wanted { + // Being told about something you never asked for would write a file describing a + // machine this one has no business talking to. + problems = append(problems, fmt.Sprintf( + "%s binds %q and does not require it", m.Module, to)) + } + } for to, where := range m.Receives { if !name.MatchString(to) { problems = append(problems, fmt.Sprintf("%q is not a usable name to receive", to)) diff --git a/internal/catalogue/resolve.go b/internal/catalogue/resolve.go index b8ff77f..ae7a38c 100644 --- a/internal/catalogue/resolve.go +++ b/internal/catalogue/resolve.go @@ -22,6 +22,9 @@ type Node struct { // — a capability that was looked for and not found is the same as one nobody looked for, as // far as deciding what may run here goes. Capabilities map[string]bool + // At is this machine's own name on the private network, empty if it is not on one. Needed to + // tell whether it can reach the node answering its requirements at all. + At string } // World is what the rest of the mesh already has. @@ -31,8 +34,8 @@ type Node struct { type World struct { // Held is the claims already taken, for the scopes wider than one node. Held []Held - // Offered is what other nodes provide at mesh scope: the name, and which nodes provide it. - Offered map[string][]string + // Offered is what other nodes provide at mesh scope, and everything needed to use it. + Offered map[string][]Provider // Pinned is which node this machine was told to get a provision from, by name. Only consulted // when more than one node could answer -- a choice recorded before it was needed should not // start meaning something the day a second provider appears, and one recorded and then made @@ -48,6 +51,17 @@ type World struct { Unchecked bool } +// Provider is one node answering a mesh-scoped requirement. +type Provider struct { + // Node is the machine. + Node string + // At is its name on the private network, or empty if it is not on one. The mesh's half of + // the answer: a module can say it serves on port 5432, and only the mesh knows where. + At string + // Serves is what the providing module said a consumer needs to know, settled. + Serves map[string]any +} + // Held is a claim somebody already has, used for the scopes wider than one node. type Held struct { Claim string @@ -80,6 +94,10 @@ type Needed struct { Name string // From is the node providing it. From string + // At is where that node is on the private network. + At string + // Serves is what the providing module said a consumer needs to know. + Serves map[string]any // For is the module that wanted it. For string } @@ -189,6 +207,25 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world if brokered[want] { reported[want] = true where := world.Offered[want] + names := make([]string, 0, len(where)) + for _, p := range where { + names = append(names, p.Node) + } + sort.Strings(names) + take := func(p Provider) { + if node.At != "" && p.At == "" || node.At == "" && p.At != "" || node.At == "" && p.At == "" { + // One of them is not on the private network, so there is no path between + // them. Said here rather than discovered as a connection timing out on a + // machine that the mesh reported as configured. + problems = append(problems, fmt.Sprintf( + "%s needs %q from %s, and they are not both on the private network — "+ + "assign %s to whichever is missing it", + node.Name, want, p.Node, meshNetwork)) + return + } + needs = append(needs, Needed{Name: want, From: p.Node, At: p.At, + Serves: p.Serves, For: because[want]}) + } switch { case world.Unchecked: // First pass. Whether anything answers this is exactly the question this pass @@ -204,42 +241,40 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world "nothing in this mesh provides %q, wanted by %s %s", want, because[want], remedy)) case len(where) == 1: - if chosenNode, pinned := world.Pinned[want]; pinned && chosenNode != where[0] { + if chosenNode, pinned := world.Pinned[want]; pinned && chosenNode != where[0].Node { // One provider, and it is not the one this machine was told to use. Silently // using the other would be the mesh overruling a choice somebody made. problems = append(problems, fmt.Sprintf( "%s was told to get %q from %s, and only %s provides it", - node.Name, want, chosenNode, where[0])) + node.Name, want, chosenNode, where[0].Node)) break } - needs = append(needs, Needed{Name: want, From: where[0], For: because[want]}) + take(where[0]) default: - sorted := append([]string{}, where...) - sort.Strings(sorted) chosenNode, pinned := world.Pinned[want] if !pinned { problems = append(problems, fmt.Sprintf( "%d nodes provide %q, wanted by %s — say which with `pin %s %s `: %s", len(where), want, because[want], node.Name, want, - strings.Join(sorted, ", "))) + strings.Join(names, ", "))) break } - var offers bool - for _, w := range where { - if w == chosenNode { - offers = true + var chosen *Provider + for i, w := range where { + if w.Node == chosenNode { + chosen = &where[i] } } - if !offers { + if chosen == nil { // Pointed at a machine that does not answer this. Refused rather than // falling back to another: a fallback would quietly move somebody's data to // a machine they did not choose, which is the whole reason this is asked. problems = append(problems, fmt.Sprintf( "%s was told to get %q from %s, and %s does not provide it — these do: %s", - node.Name, want, chosenNode, chosenNode, strings.Join(sorted, ", "))) + node.Name, want, chosenNode, chosenNode, strings.Join(names, ", "))) break } - needs = append(needs, Needed{Name: want, From: chosenNode, For: because[want]}) + take(*chosen) } continue } @@ -438,6 +473,25 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) { var out []map[string]any for _, m := range r.Modules { resources := m.Resources + for _, to := range sortedKeys(m.Binds) { + var found *Needed + for i, n := range r.Needs { + if n.Name == to { + found = &r.Needs[i] + } + } + if found == nil { + // Bound to something answered on this machine rather than from the mesh. Nothing + // to write: the answer is here, and a file saying "it is on this node" would be + // a fact nobody needs and one more thing to keep true. + continue + } + file, err := boundFile(*found, m.Binds[to]) + if err != nil { + return nil, err + } + resources = append(append([]map[string]any{}, resources...), file) + } for _, to := range sortedKeys(m.Receives) { file, err := receivedFile(to, m.Receives[to], given[to]) if err != nil { @@ -579,3 +633,34 @@ func FromAnywhere(names ...string) []Offer { } return out } + +// meshNetwork is what to assign to a machine that needs to reach another one. +// +// A string here rather than an import, because the private network is a module the control plane +// ships and this package must not depend on the thing it resolves. The name being wrong would +// show up as a refusal naming a module nobody can assign, which a test checks. +const meshNetwork = "networking" + +// boundFile is what a module is told about something it requires from another machine. +// +// Where it is and what the providing module said about using it. **No credential**, and the file +// says so rather than leaving a reader to wonder whether one was meant to be there — a missing +// field looks like a bug, and a stated absence looks like a boundary. +func boundFile(n Needed, path string) (map[string]any, error) { + body, err := json.MarshalIndent(map[string]any{ + "binding": 1, + "provision": n.Name, + "from": n.From, + "at": n.At, + "serves": n.Serves, + "generated": "by the mesh — do not edit; replaced whenever this changes. " + + "It carries no credential: the mesh has no way to issue one yet", + }, "", " ") + if err != nil { + return nil, err + } + return map[string]any{ + "id": BoundID(n.Name), "type": "file", "path": path, "mode": "0644", + "content": string(body) + "\n", + }, nil +} diff --git a/internal/catalogue/settings.go b/internal/catalogue/settings.go index 3ae9bb7..ef1fc8d 100644 --- a/internal/catalogue/settings.go +++ b/internal/catalogue/settings.go @@ -85,6 +85,12 @@ func ApplySettings(resource map[string]any, layers []Layer) (map[string]any, err return out, nil } +// Settle lays settings over a module's own values. Exported for what a provider serves, which is +// settled where the mesh is walked rather than where a node is declared. +func Settle(base map[string]any, layers []Layer) (map[string]any, error) { + return settle(base, layers, nil, "what is served") +} + // settle lays the layers over a module's own values, in order. // // Shared by a file's content and a module's contributions, because they are the same act: the