- mesh-dns-resolver: a mesh seat delivering wildcard-resolution, so every node's resolver
configuration resolves to its one holder; node-dns-resolver kept until nothing claims it.
- ${bound:<provision>:address}: the providing machine's private address, for the one consumer
that cannot use a name — a machine's resolver configuration.
- zone: a module declares the zone it answers and the listen that answers it; the controller
settles it per node, refuses duplicates and shadowing, and hands the resolver .Zones to forward.
- node-hosts-file: a node seat whose holder owns /etc/hosts, with entries/add/remove.
The resolver tests follow the catalogue: no runtime dns (containers copy the machine's resolvers),
live-restore held by resolv-conf, resolv.conf naming the resolver by address then a public one.
173 lines
5.8 KiB
Go
173 lines
5.8 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`, `from` and `address` (the providing machine's private address, novox/hq ADR 0194) 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 {
|
|
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
|
|
}
|
|
values := map[string]string{
|
|
"at": n.At,
|
|
"from": n.From,
|
|
"as": ConsumerIdentity(node, IdentitySource(m.Slug, m.Module)),
|
|
}
|
|
for key, value := range n.Serves {
|
|
// 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
|
|
}
|
|
|
|
// 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)
|
|
}
|