${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.
291 lines
11 KiB
Go
291 lines
11 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// What a provider derives for one consumer, said once in the provider's definition and delivered
|
|
// to both ends (novox/hq ADR 0188, issue 124).
|
|
//
|
|
// A `serves` block is otherwise literal: the same values for every consumer. Where the provider
|
|
// *names the resource* — a bucket, a database, a vhost — the name is derived from who is asking,
|
|
// and before this the mesh had no channel for it. The provider recomputed it in its own code and
|
|
// every consumer transcribed it into its own definition by hand, which is a copy of somebody
|
|
// else's rule kept in agreement by nobody. One of three transcriptions was wrong for months.
|
|
//
|
|
// **The mesh learns no protocol here; it spells its own name in an alphabet it already knows.**
|
|
// The only fact a served value may name is the identity the mesh itself minted for the consumer,
|
|
// in one of two alphabets: as it was minted, and as a DNS label. Everything a provider wants
|
|
// around it — a prefix, a suffix, a separator — it writes around the placeholder, because a
|
|
// served value is a string.
|
|
|
|
// consumerFact is `${consumer:<fact>}` or `${consumer:<fact>:<alphabet>}`.
|
|
var consumerFact = regexp.MustCompile(`\$\{consumer:([a-z][a-z0-9-]*)(?::([a-z][a-z0-9-]*))?\}`)
|
|
|
|
// consumerFacts are what a served value may name about the consumer it is being derived for.
|
|
// One entry, deliberately: the identity is the one thing about a consumer the mesh itself chose,
|
|
// so it is the one thing the mesh can hand to a provider without either end guessing.
|
|
var consumerFacts = []string{"as"}
|
|
|
|
// consumerAlphabets are the ways the mesh will write that identity. `dns` is the mesh's own
|
|
// identifier with its separator written `-` instead of `_` — the whole of the difference between
|
|
// the alphabet the mesh mints in and the one buckets, vhosts and hostnames accept.
|
|
var consumerAlphabets = []string{"dns"}
|
|
|
|
// ServedTo fills a provider's served values for one consumer.
|
|
//
|
|
// `as` is the identity the mesh minted for that consumer — the same string it is told to present
|
|
// as a login. Values with no placeholder are returned exactly as they were, and a block with no
|
|
// placeholder at all is returned unchanged, so this costs nothing for the providers that derive
|
|
// nothing.
|
|
//
|
|
// Only strings carry placeholders. A number, a boolean or a nested object is a value the provider
|
|
// stated outright, and is left alone.
|
|
func ServedTo(serves map[string]any, as string) (map[string]any, error) {
|
|
if len(serves) == 0 {
|
|
return serves, nil
|
|
}
|
|
var out map[string]any
|
|
for _, key := range sortedAnyKeys(serves) {
|
|
text, ok := serves[key].(string)
|
|
if !ok || !strings.Contains(text, "${consumer:") {
|
|
continue
|
|
}
|
|
filled, err := consumerInto(text, as)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the value served as %q: %w", key, err)
|
|
}
|
|
if out == nil {
|
|
// Copied only once something actually changes: the caller's map is the manifest's,
|
|
// and a provider that derives nothing must not have it rewritten underneath it.
|
|
out = make(map[string]any, len(serves))
|
|
for k, v := range serves {
|
|
out[k] = v
|
|
}
|
|
}
|
|
out[key] = filled
|
|
}
|
|
if out == nil {
|
|
return serves, nil
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// consumerInto replaces every `${consumer:…}` in one value.
|
|
//
|
|
// **A fact or an alphabet the mesh does not have is refused, not left standing.** Written through,
|
|
// the literal `${consumer:as}` would reach a configuration file and be read as a bucket name,
|
|
// failing somewhere that names neither the module nor the mesh — the same reasoning `${bound:…}`
|
|
// is refused by (boundInto).
|
|
func consumerInto(value, as string) (string, error) {
|
|
var failed error
|
|
out := consumerFact.ReplaceAllStringFunc(value, func(match string) string {
|
|
parts := consumerFact.FindStringSubmatch(match)
|
|
fact, alphabet := parts[1], parts[2]
|
|
if fact != "as" {
|
|
if failed == nil {
|
|
failed = fmt.Errorf(
|
|
"says %s, and the mesh states %s about a consumer", match, orNothing(consumerFacts))
|
|
}
|
|
return match
|
|
}
|
|
switch alphabet {
|
|
case "":
|
|
return as
|
|
case "dns":
|
|
return asDNSLabel(as)
|
|
default:
|
|
if failed == nil {
|
|
failed = fmt.Errorf(
|
|
"says %s, and the mesh writes an identity as %s", match, orNothing(consumerAlphabets))
|
|
}
|
|
return match
|
|
}
|
|
})
|
|
if failed != nil {
|
|
return "", failed
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// asDNSLabel writes a minted identity as a DNS label.
|
|
//
|
|
// The mesh's identities are already lower-case letters, digits and `_` (ConsumerIdentity), and
|
|
// already short enough for the tightest backend they reach (CheckIdentity, twenty characters). So
|
|
// this is the separator and nothing else — no lower-casing of what is already lower case, no
|
|
// truncation to a limit the identity is already inside, no padding of a name that is already long
|
|
// enough. Each of those would be the mesh guessing at a rule it has not been given.
|
|
func asDNSLabel(as string) string {
|
|
return strings.ReplaceAll(as, "_", "-")
|
|
}
|
|
|
|
// CheckServes refuses a `serves` block that names a consumer fact or an alphabet the mesh does not
|
|
// have, when the definition is parsed rather than when a consumer is resolved.
|
|
//
|
|
// A provision nobody consumes yet still has its rule read: a definition that would be refused the
|
|
// first time somebody required it is a definition that is wrong now.
|
|
func CheckServes(m Manifest) []string {
|
|
var problems []string
|
|
for _, provision := range sortedServes(m.Serves) {
|
|
for _, key := range sortedAnyKeys(m.Serves[provision]) {
|
|
text, ok := m.Serves[provision][key].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
// A probe identity, because what is checked is the shape of the statement and not
|
|
// what any consumer is called.
|
|
if _, err := consumerInto(text, "mesh_node_module"); err != nil {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s serves %s, and the value it serves as %q %s", m.Module, provision, key, err))
|
|
}
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
func sortedServes(serves map[string]map[string]any) []string {
|
|
out := make([]string, 0, len(serves))
|
|
for k := range serves {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func sortedAnyKeys(values map[string]any) []string {
|
|
out := make([]string, 0, len(values))
|
|
for k := range values {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// derivedFor is what the provider on this machine derives for one consumer of one provision
|
|
// (novox/hq ADR 0188).
|
|
//
|
|
// Settled first, then derived: an operator may set a prefix on what the provider serves and the
|
|
// mesh still fills the consumer's half of it ([ADR 0174]). Only the keys that actually name the
|
|
// consumer are returned — the rest of a `serves` block is the same for every consumer and is
|
|
// already in the provider's own definition, so repeating it here would be a second copy to go
|
|
// stale.
|
|
//
|
|
// The first module in the resolved order that says it serves the provision answers, which is the
|
|
// choice servedOnThisMachine makes for the consumer's half. Nothing serving it on this machine is
|
|
// not an error: a contribution can reach a machine whose provider is a record or an adapter, and
|
|
// then there is nothing derived to tell.
|
|
func (r Resolution) derivedFor(provision, as string, settings SettingsBy) (map[string]any, error) {
|
|
for _, m := range r.Modules {
|
|
serves, said := m.Serves[provision]
|
|
if !said {
|
|
continue
|
|
}
|
|
var names map[string]any
|
|
for key, value := range serves {
|
|
if text, ok := value.(string); ok && strings.Contains(text, "${consumer:") {
|
|
if names == nil {
|
|
names = map[string]any{}
|
|
}
|
|
names[key] = value
|
|
}
|
|
}
|
|
if names == nil {
|
|
return nil, nil
|
|
}
|
|
settled, err := Settle(names, settings[m.Module])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s serving %s: %w", m.Module, provision, err)
|
|
}
|
|
derived, err := ServedTo(settled, as)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s serving %s to %s: %w", m.Module, provision, as, err)
|
|
}
|
|
return derived, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// notTranscribed refuses a consumer's file that writes out the value its provider derives for it,
|
|
// instead of asking for it (novox/hq ADR 0188, issue 124).
|
|
//
|
|
// **What would have caught the one wrong instance.** The object store's three consumers each wrote
|
|
// their bucket into their own configuration by hand. One of them named a predecessor's bucket, and
|
|
// nothing compared it to what the provider would actually create: the module would have
|
|
// authenticated successfully and been refused on every object, which reads like a credential fault
|
|
// and is not one. It looked authoritative for months.
|
|
//
|
|
// The test is exact and costs one string search: a definition whose file already contains the
|
|
// value the mesh is about to derive for it has written down somebody else's rule. It cannot be a
|
|
// coincidence — a derived value carries the identity the mesh minted for this very consumer on
|
|
// this very machine, which nothing else would spell out — and it cannot be checked afterwards,
|
|
// because after substitution every consumer's file contains it legitimately.
|
|
//
|
|
// Only values that actually name the consumer are judged. A provider that serves a constant under
|
|
// the same key serves the same constant to everyone, and a consumer repeating it is redundant
|
|
// rather than wrong.
|
|
func notTranscribed(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 || content == "" {
|
|
return nil
|
|
}
|
|
for _, provision := range sortedKnown(known) {
|
|
values := known[provision]
|
|
identity := values["as"]
|
|
if identity == "" {
|
|
continue
|
|
}
|
|
for _, key := range sortedStringKeys(values) {
|
|
if key == "as" {
|
|
// The login is not derived from itself, and a consumer that must present it in a
|
|
// connection string legitimately has it from `${bound:…}` — which is what it will
|
|
// be after substitution, so this would judge the substitution, not the module.
|
|
continue
|
|
}
|
|
value := values[key]
|
|
if value == "" || !namesTheConsumer(value, identity) {
|
|
continue
|
|
}
|
|
if !strings.Contains(content, value) {
|
|
continue
|
|
}
|
|
return fmt.Errorf(
|
|
"%s writes %q into %v, and that is exactly what %s derives for it — a definition "+
|
|
"keeping its own copy of somebody else's naming rule is one that can disagree "+
|
|
"with it, silently. Say ${bound:%s:%s} and be told",
|
|
module, value, resource["id"], provision, provision, key)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// namesTheConsumer is whether a derived value was built from this consumer's identity — in the
|
|
// alphabet it was minted in, or as a DNS label. A value that does not contain it was not derived
|
|
// from it, whatever else it may be.
|
|
func namesTheConsumer(value, identity string) bool {
|
|
return strings.Contains(value, identity) || strings.Contains(value, asDNSLabel(identity))
|
|
}
|
|
|
|
func sortedKnown(known map[string]map[string]string) []string {
|
|
out := make([]string, 0, len(known))
|
|
for k := range known {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func sortedStringKeys(values map[string]string) []string {
|
|
out := make([]string, 0, len(values))
|
|
for k := range values {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|