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:}` or `${consumer::}`. 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 }