${consumer:as} and ${consumer:as:dns} in a serves block are filled per
consumer at resolution, and the one filled value reaches both ends: the
consumer's binding and its ${bound:...} substitutions, and the provider's
contributions entry as `derived`. A fact or alphabet the mesh does not have
is refused at parse; a consumer whose own file already holds the derived
value is refused at resolution, naming the placeholder to write instead.
180 lines
6.1 KiB
Go
180 lines
6.1 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// The half of a connection that is not secret, put where the program reading it can find it.
|
|
//
|
|
// **The asymmetry this removes was backwards** (novox/hq 04-ISSUES/023). A sealed credential can
|
|
// be placed inside any configuration file a module writes: the module leaves a hole, the mesh
|
|
// delivers the value sealed beside it, and the host — the only thing that sees both — fills it in.
|
|
// The host and the port and the name to present are ordinary facts the mesh holds in the clear,
|
|
// and they were the ones stuck: readable only inside a JSON binding, which a program reading
|
|
// `KEY=value` cannot use.
|
|
//
|
|
// So the same shape, and simpler. These values are not secret, so **the control plane substitutes
|
|
// them itself** before the declaration is sent. Nothing new reaches the host, which learns no
|
|
// formats and gains no fields.
|
|
//
|
|
// **It stays name-agnostic** ([ADR 0027]). The mesh does not learn what a `postgres-database` is:
|
|
// `at`, `as` and `from` are facts about any provision at all, and everything else comes from what
|
|
// the provider said it serves — whose keys are agreed by the requirement's name, not by this file.
|
|
|
|
// bound is where a module says a value from one of its bindings belongs:
|
|
// ${bound:<provision>.<key>}.
|
|
var bound = regexp.MustCompile(`\$\{bound:([a-z0-9][a-z0-9.-]*[a-z0-9]):([a-z0-9][a-z0-9_-]*)\}`)
|
|
|
|
// boundUsed are the (provision, key) pairs a file's content asks for, first appearance first.
|
|
func boundUsed(content string) [][2]string {
|
|
var used [][2]string
|
|
seen := map[string]bool{}
|
|
for _, m := range bound.FindAllStringSubmatch(content, -1) {
|
|
if key := m[1] + ":" + m[2]; !seen[key] {
|
|
seen[key] = true
|
|
used = append(used, [2]string{m[1], m[2]})
|
|
}
|
|
}
|
|
return used
|
|
}
|
|
|
|
// knownFor is everything a module may name from one of its bindings.
|
|
//
|
|
// Three facts the mesh states about any provision, plus whatever the provider said it serves. A
|
|
// module may not reach a binding it does not have — the same boundary as a secret, for the same
|
|
// reason.
|
|
func knownFor(m Manifest, needs []Needed, node string) (map[string]map[string]string, error) {
|
|
out := map[string]map[string]string{}
|
|
for _, want := range m.Wants() {
|
|
for i := range needs {
|
|
n := needs[i]
|
|
if n.Name != want || n.For != m.Module {
|
|
continue
|
|
}
|
|
as := ConsumerIdentity(node, IdentitySource(m.Slug, m.Module))
|
|
values := map[string]string{
|
|
"at": n.At,
|
|
"from": n.From,
|
|
"as": as,
|
|
}
|
|
// What the provider derives for this consumer rather than for all of them
|
|
// (novox/hq ADR 0188). Filled here, the one place a provision and the module
|
|
// requiring it are both in hand.
|
|
served, err := ServedTo(n.Serves, as)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s requires %s: %w", m.Module, want, err)
|
|
}
|
|
for key, value := range served {
|
|
// The provider's own vocabulary. Rendered plainly: a port is 5432, not 5432.000000,
|
|
// which is what a float would write and what a connection string would refuse.
|
|
values[key] = plainly(value)
|
|
}
|
|
out[want] = values
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// withOwnNames adds a module's own composed names to what it may name from one binding:
|
|
// `${bound:<provision>:name}` and `:internal-name`, and for several contributions to one requirement
|
|
// `:name-<local>` / `:internal-name-<local>`. Set over anything the provider serves under those keys:
|
|
// what the module is called is the mesh's statement, not the provider's.
|
|
func withOwnNames(values map[string]string, own map[string]any) {
|
|
for _, key := range []string{"name", "internal-name"} {
|
|
if v, ok := own[key].(string); ok {
|
|
values[key] = v
|
|
}
|
|
}
|
|
many, _ := own["names"].(map[string]any)
|
|
for local, raw := range many {
|
|
names, _ := raw.(map[string]any)
|
|
for _, key := range []string{"name", "internal-name"} {
|
|
if v, ok := names[key].(string); ok {
|
|
values[key+"-"+local] = v
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// plainly renders a served value as a program would expect to read it.
|
|
func plainly(value any) string {
|
|
switch v := value.(type) {
|
|
case string:
|
|
return v
|
|
case float64:
|
|
if v == float64(int64(v)) {
|
|
return fmt.Sprintf("%d", int64(v))
|
|
}
|
|
return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", v), "0"), ".")
|
|
case bool:
|
|
return fmt.Sprintf("%t", v)
|
|
case nil:
|
|
return ""
|
|
default:
|
|
return fmt.Sprint(v)
|
|
}
|
|
}
|
|
|
|
// boundInto replaces a file's ${bound:…} placeholders with what the mesh knows.
|
|
//
|
|
// A placeholder naming something the module does not require, or a key the provider does not
|
|
// serve, is refused. Left as it was, the literal `${bound:x:y}` would be written into a
|
|
// configuration file and read as a value — a connection to a host called `${bound:x:y}`, failing
|
|
// somewhere that names neither the module nor the mesh.
|
|
func boundInto(resource map[string]any, known map[string]map[string]string, module string) error {
|
|
if fmt.Sprint(resource["type"]) != "file" {
|
|
return nil
|
|
}
|
|
content, ok := resource["content"].(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
for _, pair := range boundUsed(content) {
|
|
provision, key := pair[0], pair[1]
|
|
values, has := known[provision]
|
|
if !has {
|
|
return fmt.Errorf(
|
|
"%s has a file that says ${bound:%s:%s}, and %s does not require %q. It may name %s",
|
|
module, provision, key, module, provision, orNothing(namesOfBindings(known)))
|
|
}
|
|
value, said := values[key]
|
|
if !said {
|
|
return fmt.Errorf(
|
|
"%s asks its %s binding for %q, and what answers it says %s",
|
|
module, provision, key, orNothing(namesOfKeys(values)))
|
|
}
|
|
resource["content"] = strings.ReplaceAll(
|
|
content, fmt.Sprintf("${bound:%s:%s}", provision, key), value)
|
|
content = resource["content"].(string)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func namesOfBindings(known map[string]map[string]string) []string {
|
|
var names []string
|
|
for name := range known {
|
|
names = append(names, fmt.Sprintf("%q", name))
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
func namesOfKeys(values map[string]string) []string {
|
|
var names []string
|
|
for key := range values {
|
|
names = append(names, fmt.Sprintf("%q", key))
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
func orNothing(names []string) string {
|
|
if len(names) == 0 {
|
|
return "nothing"
|
|
}
|
|
return join(names)
|
|
}
|