Files
mesh-controller/internal/catalogue/resolve.go
T
jschoubben c3046dcf56 A provider is told who its consumers are, and a reference provisioner
Contributions were node-local, so a mesh-scoped provider — the one case
that most needs them — never heard from its consumers. A database was
given a password and no idea what to create it for.

Cross-node consumers now reach the provider's `receives` file, merged in
with the ones on its own machine: from the provider's side they are the
same thing, and a provider that had to read two lists would read one of
them. Each names the file its credential is in rather than carrying it,
because the mesh discarded the value and could not put it there. The
readable half therefore stays readable.

And examples/postgres-provisioner, which is the last step: it reads what
the host wrote and makes PostgreSQL accept it. Explicitly not part of the
control plane — the control plane decides and never touches a machine.
This runs on the machine and touches it, and a real one ships with the
module that ships PostgreSQL. It lives here because this is where the
contract is defined, written as something that runs so it can be read.

It reconciles rather than applying a change, because it is never told
what changed. Three things that follow, and each is a fault somebody has
shipped:

- the password is set every time, not only on creation, or a rotation
  reports success and changes nothing
- what it made and nobody asks for any more is revoked, or a departed
  consumer keeps a working login for ever
- what it did not make is left alone, or it cannot be run on a database
  that predates it

Proven in the lab against a real PostgreSQL, each assertion confirmed to
fail with the behaviour removed. The suite is in mesh-lab, which also
records the two ways the test itself was wrong first.
2026-08-30 01:31:25 +02:00

797 lines
30 KiB
Go

package catalogue
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
)
// Resolving is turning "these modules are assigned here" into "this is what the node runs".
//
// It refuses rather than guesses, everywhere. novox/hq ADR 0009: a requirement with several
// answers is refused and named, because counting candidates has no surprising behaviour and a
// solver that picks has to be understood before its answer can be trusted.
// Node is what resolution needs to know about the machine.
type Node struct {
Name string
Site string
// Capabilities the machine actually has, as its profile reported them. Only the present ones
// — 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.
//
// Some requirements cannot be answered on the machine that has them — a database runs somewhere
// and is reached over the network — so resolving one node needs to know what the others offer.
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, 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
// unnecessary should not quietly stop applying either.
Pinned map[string]string
// Unchecked takes brokered requirements on trust instead of refusing when nothing answers
// them.
//
// For the first of two passes. Working out what a node offers the mesh needs that node
// resolved, and resolving it may need what the mesh offers — so the first pass answers only
// *what does each node offer*, and the second pass answers everything with that in hand. A
// declaration is never built from an unchecked resolution.
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
Scope string
Node string
Module string
Site string
}
// Resolution is what a node should run, and why.
type Resolution struct {
// Node is which machine this was resolved for, so a generator can be asked about it.
Node string
// Modules in the order they were resolved: assigned first, then what they pulled in.
Modules []Manifest
// Because says why each module is here — assigned, or required by something.
Because map[string]string
// Claims is what this node's set holds, so wider scopes can be checked against it.
Claims []Held
// Needs is what this node's set gets from other nodes. Recorded rather than resolved away,
// because it is where a credential will have to be handed back once there is a mechanism for
// that, and because "what does this machine depend on that is not on it" has no other answer.
Needs []Needed
}
// Needed is one thing this node's set takes from elsewhere in the mesh.
type Needed struct {
// Name is the provision, as required.
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
// Sealed is the credential, closed to this node. Filled in after resolving, because whose
// credential it is only becomes answerable once which node answers has been settled.
Sealed string
// For is the module that wanted it.
For string
}
// Refusal is why a set of assignments cannot become a declaration.
//
// Every reason at once rather than the first, and each says what to do about it. A person
// resolving these fixes them in one pass or in four.
type Refusal struct{ Problems []string }
func (r *Refusal) Error() string {
return "these assignments cannot be applied:\n - " + strings.Join(r.Problems, "\n - ")
}
// ErrAmbiguous is returned inside a Refusal when a requirement has more than one answer.
var ErrAmbiguous = errors.New("more than one module provides that")
// Resolve works out everything a node runs, from what was assigned to it.
//
// The catalogue is every module the mesh knows about; assigned is what a person put on this node.
// What comes back is the closure — assigned modules plus everything they require — or a refusal
// naming every reason it could not be closed.
func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world World) (Resolution, error) {
elsewhere := world.Held
var problems []string
// Which names are answered from elsewhere in the mesh rather than from this machine. A
// property of the name, not of each provider: two modules disagreeing about whether a
// database is local would make the same requirement mean different things depending on which
// one happened to answer it.
brokered := map[string]bool{}
local := map[string]bool{}
for _, m := range catalogue {
for _, o := range m.Provides {
if o.At() == ScopeMesh {
brokered[o.Name] = true
continue
}
local[o.Name] = true
}
}
for want := range brokered {
if local[want] {
problems = append(problems, fmt.Sprintf(
"the catalogue disagrees about %q: some modules provide it here and others from "+
"anywhere in the mesh, so the same requirement would mean two things", want))
}
}
// What each name can be satisfied by. Built once from the whole catalogue, because "how many
// modules provide this" is the question the whole rule turns on.
offers := map[string][]string{}
for _, m := range catalogue {
for _, o := range m.Offers() {
offers[o] = append(offers[o], m.Module)
}
}
for k := range offers {
sort.Strings(offers[k])
}
chosen := map[string]bool{}
because := map[string]string{}
var order []string
var needs []Needed
// What the set already offers, which is the first thing a requirement is checked against.
//
// Without this, assigning zsh does not satisfy something that requires a shell: the
// requirement is counted against the catalogue, three modules provide it, and the answer is
// still "choose one" after somebody has chosen one. That makes the remedy useless, and it is
// how this read when first used.
satisfied := map[string]bool{}
// Everything a person assigned goes in first. Those are choices already made, and a
// requirement one of them answers is not a choice to put back to anybody.
queue := append([]string{}, assigned...)
for _, a := range assigned {
because[a] = "assigned"
if m, known := catalogue[a]; known {
for _, o := range m.Offers() {
satisfied[o] = true
}
}
}
// What has already been complained about. A requirement can be wanted by several modules at
// once, and saying the same thing twice makes a person hunt for the difference between two
// identical lines before realising there is none.
reported := map[string]bool{}
for len(queue) > 0 {
want := queue[0]
queue = queue[1:]
if chosen[want] || reported[want] {
continue
}
// Already answered by something in the set. This is the case that makes assigning zsh do
// what a person meant by it.
if satisfied[want] && !isModule(catalogue, want) {
continue
}
// Answered from elsewhere in the mesh, if it is that kind of name. **Never installed
// here.** Choosing a machine to put a database on is a decision with consequences
// nobody would want made silently by something resolving a web application.
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
// exists to make answerable, so it is not asked here.
case len(where) == 0:
remedy := "and nothing in the catalogue could"
if answers := offers[want]; len(answers) == 1 {
remedy = "— assign " + answers[0] + " to a node"
} else if len(answers) > 1 {
remedy = "— assign one of these to a node: " + strings.Join(answers, ", ")
}
problems = append(problems, fmt.Sprintf(
"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].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))
break
}
take(where[0])
default:
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 <node>`: %s",
len(where), want, because[want], node.Name, want,
strings.Join(names, ", ")))
break
}
var chosen *Provider
for i, w := range where {
if w.Node == chosenNode {
chosen = &where[i]
}
}
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(names, ", ")))
break
}
take(*chosen)
}
continue
}
candidates := offers[want]
switch len(candidates) {
case 0:
reported[want] = true
problems = append(problems, fmt.Sprintf(
"nothing provides %q, wanted by %s", want, because[want]))
continue
case 1:
// No choice to make, so none is made. This is the case that lets `install i3` bring
// in xorg without anybody being asked anything.
default:
reported[want] = true
problems = append(problems, fmt.Sprintf(
"%q is wanted by %s and %d modules provide it — choose one and assign it: %s",
want, because[want], len(candidates), strings.Join(candidates, ", ")))
continue
}
m := catalogue[candidates[0]]
if chosen[m.Module] {
continue
}
chosen[m.Module] = true
order = append(order, m.Module)
for _, o := range m.Offers() {
satisfied[o] = true
}
if _, ok := because[m.Module]; !ok {
because[m.Module] = fmt.Sprintf("required by %s", because[want])
}
for _, r := range m.Wants() {
if _, ok := because[r]; !ok {
because[r] = m.Module
}
queue = append(queue, r)
}
}
resolution := Resolution{Node: node.Name, Because: because, Needs: needs}
for _, n := range order {
resolution.Modules = append(resolution.Modules, catalogue[n])
}
problems = append(problems, checkCapabilities(resolution.Modules, node)...)
claims, claimProblems := checkClaims(resolution.Modules, node, elsewhere)
problems = append(problems, claimProblems...)
problems = append(problems, checkResources(resolution.Modules)...)
resolution.Claims = claims
if len(problems) > 0 {
sort.Strings(problems)
return Resolution{}, &Refusal{Problems: problems}
}
return resolution, nil
}
// isModule reports whether a name is a module in its own right rather than only something
// modules provide.
//
// A requirement naming a module is not satisfied by something else providing that name: `i3`
// requires `xorg` and means xorg, not "anything calling itself a display server".
func isModule(catalogue map[string]Manifest, want string) bool {
_, ok := catalogue[want]
return ok
}
// checkCapabilities refuses a module the machine cannot run.
//
// Said as a fact about the machine rather than about the module, because that is what it is and
// because nothing can be installed to change it.
func checkCapabilities(modules []Manifest, node Node) []string {
var problems []string
for _, m := range modules {
for _, c := range m.Capabilities {
if !node.Capabilities[c] {
problems = append(problems, fmt.Sprintf(
"%s needs the capability %q and %s does not have it — this is the wrong "+
"machine, not a missing module", m.Module, c, node.Name))
}
}
}
return problems
}
// checkClaims refuses two modules holding one singular thing.
//
// Within this node's own set, and against what is already held elsewhere for the wider scopes. A
// claim at mesh scope is the same idea as the mesh's one hub, said once instead of hard-coded.
func checkClaims(modules []Manifest, node Node, elsewhere []Held) ([]Held, []string) {
var problems []string
var held []Held
byScope := map[string]map[string]string{} // scope → claim → module
for _, m := range modules {
for _, c := range m.Claims {
scope := c.At()
if byScope[scope] == nil {
byScope[scope] = map[string]string{}
}
if other, taken := byScope[scope][c.Name]; taken {
problems = append(problems, fmt.Sprintf(
"%s and %s both claim %q, and only one thing may hold it per %s",
other, m.Module, c.Name, scope))
continue
}
byScope[scope][c.Name] = m.Module
held = append(held, Held{Claim: c.Name, Scope: scope, Node: node.Name,
Module: m.Module, Site: node.Site})
}
}
// And against the rest of the mesh, for the scopes that reach past this machine.
for _, h := range held {
for _, e := range elsewhere {
if e.Node == node.Name || e.Claim != h.Claim || e.Scope != h.Scope {
continue
}
switch h.Scope {
case ScopeMesh:
problems = append(problems, fmt.Sprintf(
"%s on %s claims %q, which %s on %s already holds — one per mesh",
h.Module, node.Name, h.Claim, e.Module, e.Node))
case ScopeSite:
if node.Site != "" && node.Site == e.Site {
problems = append(problems, fmt.Sprintf(
"%s on %s claims %q, which %s on %s already holds at %s — one per site",
h.Module, node.Name, h.Claim, e.Module, e.Node, node.Site))
}
}
}
}
return held, problems
}
// checkResources refuses two modules writing the same thing.
//
// This costs no manifest field: the mesh already holds every resource of every module, so two
// declaring one path or one unit are visible without either having to know about the other. A
// declared claim is only for the abstract conflicts nothing in the resources reveals.
func checkResources(modules []Manifest) []string {
var problems []string
owner := map[string]string{}
for _, m := range modules {
for _, r := range m.Resources {
for _, field := range []string{"path", "unit", "name", "package"} {
value, ok := r[field].(string)
if !ok || value == "" {
continue
}
key := field + " " + value
if other, taken := owner[key]; taken && other != m.Module {
problems = append(problems, fmt.Sprintf(
"%s and %s both declare the %s %q", other, m.Module, field, value))
}
owner[key] = m.Module
}
}
}
return problems
}
// SettingsBy is the layers that apply to each module, keyed by module name.
type SettingsBy map[string][]Layer
// Generator works out a module's resources for one node, where they cannot be written in advance.
type Generator interface {
// Resources for this node. Absent means the node is not part of whatever this generates,
// which is an ordinary answer rather than a failure — a machine assigned the module before it
// has an address on the network is in exactly that state.
Resources(node string) ([]map[string]any, bool, error)
}
// Grant is one consumer's credential, on the machine that must create it.
type Grant struct {
// Provision is what was required.
Provision string
// Consumer is the node that will use it, which is also what names the file.
Consumer string
// From is the module on that machine which asked, so the provider can name what it creates
// after the thing using it rather than after the machine.
From string
// Values are what that module contributed — the name it wants, and anything else the
// provision's own vocabulary defines.
Values map[string]any
// Sealed is the credential, closed to the providing node.
Sealed string
}
// Rendering is everything needed to turn a resolution into the declaration a node is sent.
type Rendering struct {
Settings SettingsBy
Generators map[string]Generator
// Grants are the credentials this node must create, for the provisions it offers. Passed in
// rather than resolved, because who consumes a node is a fact about the rest of the mesh and
// resolution answers questions about one machine.
Grants []Grant
}
// Declaration is everything the resolved modules put on the node, with settings applied.
//
// Resource identities are prefixed with the module they came from. Two modules may reasonably
// both call something "config", and without this the second would silently replace the first —
// the node applying one of them and reporting success.
func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
// Where each provision's credentials land, so a contribution can name the file rather than
// carry a value the mesh does not have.
directories := map[string]string{}
for _, m := range r.Modules {
for provision, where := range m.Grants {
directories[provision] = where
}
}
given, err := r.contributions(with.Settings, with.Grants, directories)
if err != nil {
return nil, err
}
var out []map[string]any
for _, m := range r.Modules {
resources := m.Resources
for _, to := range sortedKeys(m.Secrets) {
var found *Needed
for i, n := range r.Needs {
if n.Name == to {
found = &r.Needs[i]
}
}
if found == nil || found.Sealed == "" {
// Answered on this machine, or answered by a node the mesh could not seal to.
// Nothing to write either way, and writing an empty credential file would be
// worse than none: something would read it and fail authenticating.
continue
}
resources = append(append([]map[string]any{}, resources...), map[string]any{
"id": SecretID(to), "type": "file", "path": m.Secrets[to],
"sealed": found.Sealed,
})
}
for _, to := range sortedKeys(m.Grants) {
for _, g := range with.Grants {
if g.Provision != to {
continue
}
resources = append(append([]map[string]any{}, resources...), map[string]any{
"id": GrantID(to, g.Consumer),
"type": "file",
"path": grantPath(m.Grants[to], g.Consumer),
"sealed": g.Sealed,
})
}
}
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 {
return nil, err
}
resources = append(append([]map[string]any{}, resources...), file)
}
if m.Computed != "" {
generator, known := with.Generators[m.Computed]
if !known {
return nil, fmt.Errorf(
"%s says its resources are computed by %q, and this control plane has no %q",
m.Module, m.Computed, m.Computed)
}
generated, part, err := generator.Resources(r.Node)
if err != nil {
return nil, err
}
if !part {
// Assigned, and not yet part of what this generates. Nothing to put on the
// machine, which is different from an error: a node given the network module
// before it has an address is in exactly that state, briefly.
continue
}
resources = generated
}
for _, unsettled := range resources {
resource, err := ApplySettings(unsettled, with.Settings[m.Module])
if err != nil {
return nil, err
}
copied := map[string]any{}
for k, v := range resource {
copied[k] = v
}
copied["id"] = m.Module + "." + fmt.Sprint(resource["id"])
// A service saying what it reflects names resources within its own module, so those
// are prefixed too or they would point at nothing.
if reflects, ok := resource["restart-on"].([]any); ok {
var renamed []any
for _, id := range reflects {
renamed = append(renamed, m.Module+"."+fmt.Sprint(id))
}
copied["restart-on"] = renamed
}
out = append(out, copied)
}
}
return out, nil
}
// Contribution is one module telling the answer to a requirement what it needs from it.
type Contribution struct {
// From is the module that said it, so the provider and a person reading the file can tell
// which route belongs to what.
From string `json:"from"`
// Node is the machine it said it from, empty when that is this one.
//
// A provision answered from anywhere in the mesh has consumers on other machines, and the
// provider has to know who they are — a database told to create a password and not who for
// cannot do anything with it. Contributions were node-local until this, which meant the one
// case that most needed them was the one they did not reach.
Node string `json:"node,omitempty"`
// Secret is the file on this machine holding that consumer's credential, sealed to it.
//
// Named rather than carried, for the same reason the private network's key is: the mesh
// discarded the value and could not put it here if it wanted to. What is here is where to
// find it.
Secret string `json:"secret,omitempty"`
// Values are the module's own, with settings applied. What the keys mean is agreed by the
// requirement's name — everything providing `reverse-proxy` understands the same shape, which
// is what makes swapping one for another cost nothing.
Values map[string]any `json:"values"`
}
// grantPath is where one consumer's sealed credential lands on the providing machine.
//
// Suffixed, so the directory can also hold whatever the module writing it keeps there and so a
// node named like something else in that directory cannot collide with it.
func grantPath(directory, consumer string) string {
return strings.TrimRight(directory, "/") + "/" + consumer + ".secret"
}
// contributions collects what every module in this set contributes, by requirement.
//
// Ordered by contributing module, because the result becomes a file on a machine and a file whose
// lines move about is a file that looks changed when nothing changed.
func (r Resolution) contributions(settings SettingsBy, grants []Grant,
directories map[string]string) (map[string][]Contribution, error) {
out := map[string][]Contribution{}
modules := append([]Manifest{}, r.Modules...)
sort.Slice(modules, func(i, j int) bool { return modules[i].Module < modules[j].Module })
// What consumers on other machines asked for. Merged in with this machine's own, because from
// the provider's side they are the same thing — somebody wanting something — and a provider
// that had to read two lists would be a provider that reads one of them.
sorted := append([]Grant{}, grants...)
sort.Slice(sorted, func(i, j int) bool {
if sorted[i].Provision != sorted[j].Provision {
return sorted[i].Provision < sorted[j].Provision
}
return sorted[i].Consumer < sorted[j].Consumer
})
for _, g := range sorted {
out[g.Provision] = append(out[g.Provision], Contribution{
From: g.From, Node: g.Consumer, Values: g.Values,
Secret: grantPath(directories[g.Provision], g.Consumer),
})
}
for _, m := range modules {
for _, to := range sortedKeys(m.Contributes) {
// Settings reach a contribution the same way they reach a file. A route's hostname is
// exactly the kind of thing that differs between one mesh and the next, and a module
// that could not have it set would have to be edited to be reused.
values, err := settle(m.Contributes[to], settings[m.Module], nil,
m.Module+" contributing to "+to)
if err != nil {
return nil, fmt.Errorf("%s contributing to %s: %w", m.Module, to, err)
}
out[to] = append(out[to], Contribution{From: m.Module, Values: values})
}
}
return out, nil
}
// receivedFile is the file a provider is given its consumers' contributions in.
func receivedFile(requirement, path string, given []Contribution) (map[string]any, error) {
if given == nil {
// Nobody contributed. The file is still written, empty, rather than left absent: a
// provider that finds no file cannot tell "nothing asked for me" from "the mesh never
// wrote it", and the two want completely different responses.
given = []Contribution{}
}
// The note goes *inside* the document, not above it. The first version wrote a `//` header
// and produced a file that says "do not edit" to a person and fails to parse for the program
// meant to read it — which is the whole audience.
body, err := json.MarshalIndent(map[string]any{
"contributions": 1,
"requirement": requirement,
"generated": "by the mesh — do not edit; replaced whenever a module contributing to " +
requirement + " arrives or leaves",
"given": given,
}, "", " ")
if err != nil {
return nil, err
}
return map[string]any{
"id": ReceivedID(requirement), "type": "file", "path": path, "mode": "0644",
"content": string(body) + "\n",
}, nil
}
// sortedKeys is map iteration made repeatable, which everything written to a machine needs.
func sortedKeys[V any](m map[string]V) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}
// Offers is a list of node-scoped provisions, which is what nearly everything is.
func Offers(names ...string) []Offer {
out := make([]Offer, 0, len(names))
for _, n := range names {
out = append(out, Offer{Name: n})
}
return out
}
// FromAnywhere is a provision answered by whichever node in the mesh runs it.
func FromAnywhere(names ...string) []Offer {
out := make([]Offer, 0, len(names))
for _, n := range names {
out = append(out, Offer{Name: n, Scope: ScopeMesh})
}
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
}
// ContributionsTo is what this node's set asked of one requirement, settled.
//
// Exported because a provider's grants are assembled from its consumers' resolutions, one machine
// at a time, and the alternative was for the control plane to reimplement settling.
func (r Resolution) ContributionsTo(requirement string, settings SettingsBy) (
string, map[string]any, error) {
all, err := r.contributions(settings, nil, nil)
if err != nil {
return "", nil, err
}
given := all[requirement]
if len(given) == 0 {
return "", nil, nil
}
if len(given) > 1 {
// Two modules on one machine wanting the same provision would share one credential, and
// the provider would be told to create one thing under two names. Refused rather than
// resolved by picking, which is the rule everywhere else here.
var who []string
for _, g := range given {
who = append(who, g.From)
}
sort.Strings(who)
return "", nil, fmt.Errorf(
"%s has %d modules asking for %q and they would share one credential: %s",
r.Node, len(given), requirement, strings.Join(who, ", "))
}
return given[0].From, given[0].Values, nil
}