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::}, and for an optional requirement the value it takes while absent, // ${bound::|default:} (novox/hq ADR 0330 §4). A default is never empty: an // absent requirement is never rendered as an empty string. var bound = regexp.MustCompile(`\$\{bound:([a-z0-9][a-z0-9.-]*[a-z0-9]):([a-z0-9][a-z0-9_-]*)(?:\|default:([^}\n]+))?\}`) // 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 0202). 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::name}` and `:internal-name`, and for several contributions to one requirement // `:name-` / `:internal-name-`. 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. // // **An optional requirement left absent takes the default its placeholder names** (novox/hq ADR // 0330 §4), and one that names none is refused rather than rendered empty. The module check refuses // that already; this is the same rule where the file is made, for a manifest registered before it. func boundInto(resource map[string]any, known map[string]map[string]string, module string, absent map[string]bool) error { if fmt.Sprint(resource["type"]) != "file" { return nil } content, ok := resource["content"].(string) if !ok { return nil } var refused error filled := bound.ReplaceAllStringFunc(content, func(token string) string { if refused != nil { return token } m := bound.FindStringSubmatch(token) provision, key, fallback := m[1], m[2], m[3] values, has := known[provision] if !has { if absent[provision] { if fallback != "" { return fallback } refused = fmt.Errorf( "%s has a file that says ${bound:%s:%s}, and %q is optional and absent here: it names no "+ "value for then (${bound:%s:%s|default:}), so it would be written empty "+ "(novox/hq ADR 0330 §4)", module, provision, key, provision, provision, key) return token } refused = 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))) return token } value, said := values[key] if !said { refused = fmt.Errorf( "%s asks its %s binding for %q, and what answers it says %s", module, provision, key, orNothing(namesOfKeys(values))) return token } return value }) if refused != nil { return refused } resource["content"] = filled 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) }