The key-value-buckets record took 0201 on main while this waited to merge. Only the comments citing the derived-value work move; this repository's other 0201 citations are the buckets record's own and stay.
312 lines
12 KiB
Go
312 lines
12 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 0202, 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 0202).
|
|
//
|
|
// 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, consumer, local 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
|
|
}
|
|
// **A consumer that keeps several holders of this provision is refused** — this is issue
|
|
// 124's own failure one case to the side, and it would be just as quiet.
|
|
//
|
|
// Each holder gets its own login, `…_<local>` (ADR 0094), and a provider derives from the
|
|
// login, so it would make one resource per holder. The consumer's side has no such
|
|
// dimension: one binding file per provision, one `${bound:<provision>:<key>}`, both
|
|
// derived from the un-suffixed identity. So the provider would create the holder's
|
|
// resource and the consumer would be configured against a name nothing made — it would
|
|
// authenticate successfully and be refused on every object, which reads like a credential
|
|
// fault and is not one.
|
|
//
|
|
// Lifting this means giving the consumer's side a local dimension. That is a decision,
|
|
// not an omission, and until it is taken the mesh says so rather than guessing.
|
|
if local != "" {
|
|
return nil, fmt.Errorf(
|
|
"%s keeps several holders of %s (this one is %q), and %s derives %s for each "+
|
|
"consumer from the login the mesh minted. Each holder has its own login, and a "+
|
|
"consumer is told one value per requirement — so the two ends would name "+
|
|
"different things and nothing would compare them (novox/hq ADR 0202)",
|
|
consumer, local, provision, m.Module, orNothing(sortedAnyKeys(names)))
|
|
}
|
|
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 0202, 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
|
|
}
|