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` and `from` 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:.}. 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, 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 } // 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) }