A consumer can write its own connection string
novox/hq 04-ISSUES/023. A consumer was given its password, the address,
the port and where its credential lives, and still could not connect —
the user name was invented by the provisioner and recorded nowhere, and
the rest sat in a JSON binding that a program reading KEY=value cannot
use.
Both halves have the same cause: the mesh knew something and did not say
it.
**Who a consumer is, said once.** The provisioner used to derive
mesh_<node>_<module> and that string existed nowhere else — not in the
control plane, not in the binding, and above all not at the consumer,
which has to present it. Now the mesh derives it once and sends it to
both ends, so they agree by construction rather than by two conventions
that were the same on the day they were written. The provisioners refuse
to invent one if the mesh says nothing, because falling back to a name
of their own would create a role the consumer would never guess and
everything would report success.
**Bound values reach the file that needs them.** ${bound:provision:key}
is the symmetric twin of the sealed placeholder, and simpler: these
values are not secret, so the control plane fills them in before sending
and the host gains no field and learns no format. It stays
name-agnostic — at, as and from are true of any provision, and every
other key comes from what the provider said it serves.
The asymmetry it removes was backwards. The secret is the hard case,
because the mesh must not be able to read it, and the secret was the
part that already arrived.
Keycloak and Gitea now produce complete connections, asserted from the
manifests on disk rather than from fixtures: every part filled, no
placeholder surviving as a value, and the password still a hole only the
host can close. Three faults injected, each caught.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
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:<provision>.<key>}.
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user