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:
2026-09-01 03:03:07 +02:00
parent 96f90ab986
commit 122680b554
10 changed files with 550 additions and 19 deletions
+150
View File
@@ -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)
}
+170
View File
@@ -0,0 +1,170 @@
package catalogue
import (
"strings"
"testing"
)
// A consumer writes its own connection string (novox/hq 04-ISSUES/023).
//
// Everything needed for one now reaches the file that needs it: the address and port from what the
// provider serves, the name to present from what the mesh decided, the database name from what the
// module itself contributed, and the password through the sealed hole the host fills.
func TestAConsumerCanWriteAConnectionString(t *testing.T) {
r := Resolution{
Node: "workstation",
Modules: []Manifest{{
Module: "keycloak",
Requires: []string{"postgres-database"},
Secrets: map[string]string{"postgres-database": "/var/lib/keycloak/db.secret"},
Resources: []map[string]any{{
"id": "dbenv", "type": "file", "path": "/var/lib/keycloak/database.env",
"mode": "0600",
"content": "KC_DB_URL=jdbc:postgresql://${bound:postgres-database:at}:" +
"${bound:postgres-database:port}/keycloak\n" +
"KC_DB_USERNAME=${bound:postgres-database:as}\n" +
"KC_DB_PASSWORD=${secret:postgres-database}\n",
}},
}},
Needs: []Needed{{
Name: "postgres-database", From: "anchor", At: "anchor.internal", For: "keycloak",
Sealed: "sealed-db", Serves: map[string]any{"port": float64(5432)},
}},
}
out, err := r.Declaration(Rendering{})
if err != nil {
t.Fatal(err)
}
file := fileNamed(out, "keycloak.dbenv")
if file == nil {
t.Fatalf("no file in %v", out)
}
got := file["content"].(string)
want := "KC_DB_URL=jdbc:postgresql://anchor.internal:5432/keycloak\n" +
"KC_DB_USERNAME=mesh_workstation_keycloak\n" +
"KC_DB_PASSWORD=${secret:postgres-database}\n"
if got != want {
t.Errorf("the consumer cannot connect with what it was given:\n got %q\nwant %q", got, want)
}
// The password is still a hole. Only the host may fill that one, and only on the machine.
if sealed, _ := file["secrets"].(map[string]any); sealed["postgres-database"] != "sealed-db" {
t.Errorf("the credential did not travel with the file: %v", file)
}
}
// A port is 5432, not 5432.000000 — which is what a number decoded from JSON would write, and what
// every connection string in the world would refuse.
func TestAPortIsWrittenAsAPort(t *testing.T) {
if got := plainly(float64(5432)); got != "5432" {
t.Errorf("a port was rendered as %q", got)
}
if got := plainly("postgres"); got != "postgres" {
t.Errorf("a string was rendered as %q", got)
}
}
// The name the provider will create is the name the consumer is told to present. One derivation,
// so the two agree by construction rather than by two conventions written on the same day.
func TestTheProviderAndTheConsumerAgreeOnTheName(t *testing.T) {
provider, err := Resolve(shelf(Manifest{
Module: "postgres", Version: "1", Provides: FromAnywhere("postgres-database"),
Grants: map[string]string{"postgres-database": "/var/lib/postgres/grants"},
Receives: map[string]string{"postgres-database": "/var/lib/postgres/grants/mesh.json"},
}), []string{"postgres"}, reachable(), World{})
if err != nil {
t.Fatal(err)
}
out, err := provider.Declaration(Rendering{Grants: []Grant{{
Provision: "postgres-database", Consumer: "workstation", From: "keycloak",
Values: map[string]any{"name": "keycloak"}, Sealed: "sealed",
}}})
if err != nil {
t.Fatal(err)
}
var told string
for _, res := range out {
if res["path"] == "/var/lib/postgres/grants/mesh.json" {
told = res["content"].(string)
}
}
if !strings.Contains(told, `"as": "mesh_workstation_keycloak"`) {
t.Errorf("the provider was not told what to call the login: %s", told)
}
if told == "" {
t.Fatal("the provider was told nothing at all")
}
// Which is exactly what the consumer, on its own machine, is told to present.
if ConsumerIdentity("workstation", "keycloak") != "mesh_workstation_keycloak" {
t.Errorf("got %q", ConsumerIdentity("workstation", "keycloak"))
}
}
// A binding the module does not have is a typo, refused where it was written.
func TestAFileCannotNameABindingTheModuleDoesNotHave(t *testing.T) {
r := Resolution{
Node: "workstation",
Modules: []Manifest{{
Module: "keycloak", Requires: []string{"postgres-database"},
Resources: []map[string]any{{
"id": "conf", "type": "file", "path": "/etc/keycloak.conf",
"content": "host=${bound:s3-bucket:at}\n",
}},
}},
Needs: []Needed{{Name: "postgres-database", From: "anchor", At: "a", For: "keycloak"}},
}
_, err := r.Declaration(Rendering{})
if err == nil {
t.Fatal("a module read a binding it does not have")
}
if !strings.Contains(err.Error(), "postgres-database") {
t.Errorf("the refusal does not say what it could have named: %v", err)
}
}
// A key the provider does not serve is refused too. Written through, the literal placeholder
// becomes a hostname, and the failure names neither the module nor the mesh.
func TestAKeyTheProviderDoesNotServeIsRefused(t *testing.T) {
r := Resolution{
Node: "workstation",
Modules: []Manifest{{
Module: "keycloak", Requires: []string{"postgres-database"},
Resources: []map[string]any{{
"id": "conf", "type": "file", "path": "/etc/keycloak.conf",
"content": "sock=${bound:postgres-database:socket}\n",
}},
}},
Needs: []Needed{{
Name: "postgres-database", From: "anchor", At: "a", For: "keycloak",
Serves: map[string]any{"port": float64(5432)},
}},
}
_, err := r.Declaration(Rendering{})
if err == nil {
t.Fatal("a module read a key nothing serves, and the placeholder became a value")
}
if !strings.Contains(err.Error(), `"port"`) {
t.Errorf("the refusal does not say what is served: %v", err)
}
}
// A name too long to stay distinct is refused before anything is created.
func TestAnIdentityTooLongToBeDistinctIsRefused(t *testing.T) {
if err := CheckIdentity("anchor", "keycloak"); err != nil {
t.Fatalf("an ordinary consumer was refused: %v", err)
}
if err := CheckIdentity(strings.Repeat("n", 40), strings.Repeat("m", 40)); err == nil {
t.Fatal("an identity that providers would shorten was accepted")
}
}
// Two modules on one node get two identities, which is the whole point of both issues.
func TestTwoModulesOnOneNodeAreTwoIdentities(t *testing.T) {
if ConsumerIdentity("anchor", "gitea") == ConsumerIdentity("anchor", "keycloak") {
t.Fatal("two consumers on one machine share an identity")
}
// And one module on two nodes, likewise.
if ConsumerIdentity("anchor", "gitea") == ConsumerIdentity("laptop", "gitea") {
t.Fatal("one module on two machines shares an identity")
}
}
+23 -3
View File
@@ -263,7 +263,7 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
}
found = here
}
file, err := boundFile(*found, m.Binds[to])
file, err := boundFile(*found, m.Binds[to], ConsumerIdentity(r.Node, m.Module))
if err != nil {
return nil, err
}
@@ -317,6 +317,8 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
if err != nil {
return nil, err
}
// And what its bindings say, for the half of a connection that is not secret.
known := knownFor(m, r.Needs, r.Node)
for _, unsettled := range resources {
resource, err := ApplySettings(unsettled, with.Settings[m.Module])
@@ -334,6 +336,12 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
if err := intoFile(copied, sealed, m.Module); err != nil {
return nil, err
}
// **Substituted here, not on the machine.** A bound value is not secret — the mesh
// holds it in the clear — so there is nothing for the host to be the only witness of,
// and sending it already filled in means the host learns no new field.
if err := boundInto(copied, known, m.Module); err != nil {
return nil, err
}
copied["id"] = m.Module + "." + fmt.Sprint(resource["id"])
// A service saying what it reflects names resources within its own module, so those
// are prefixed too or they would point at nothing.
@@ -382,6 +390,13 @@ type Contribution struct {
// provider that reaches back to a consumer would have to know how the mesh names machines,
// which is a convention leaking into every module that implements a provision.
At string `json:"at,omitempty"`
// As is who this consumer is: what the provider should call the login it creates.
//
// **Said by the mesh rather than invented by the provisioner** (novox/hq 04-ISSUES/023). It
// used to be neither — the provisioner made a name, and the consumer, which has to present it
// to authenticate, had no way to learn it. One derivation reaches both ends, so they agree by
// construction.
As string `json:"as"`
// Secret is the file on this machine holding that consumer's credential, sealed to it.
//
// Named rather than carried, for the same reason the private network's key is: the mesh
@@ -438,6 +453,7 @@ func (r Resolution) contributions(settings SettingsBy, grants []Grant,
}
out[g.Provision] = append(out[g.Provision], Contribution{
From: g.From, Node: g.Consumer, At: g.At, Values: g.Values,
As: ConsumerIdentity(g.Consumer, g.From),
Secret: grantPath(directories[g.Provision], g.Consumer, g.From),
})
}
@@ -499,7 +515,7 @@ func sortedKeys[V any](m map[string]V) []string {
// Where it is and what the providing module said about using it. **No credential**, and the file
// says so rather than leaving a reader to wonder whether one was meant to be there — a missing
// field looks like a bug, and a stated absence looks like a boundary.
func boundFile(n Needed, path string) (map[string]any, error) {
func boundFile(n Needed, path, as string) (map[string]any, error) {
// A record has no machine and no address. Saying so is the difference between a reader
// concluding "somewhere with no address" and concluding the mesh failed to fill something in.
where := any(n.At)
@@ -511,7 +527,11 @@ func boundFile(n Needed, path string) (map[string]any, error) {
"provision": n.Name,
"from": n.From,
"at": where,
"serves": n.Serves,
// Who this module is at the other end. **The half that was missing** (novox/hq
// 04-ISSUES/023): a consumer was told the address, the port and where its password is,
// and not the name it must present — which the provisioner had invented.
"as": as,
"serves": n.Serves,
// **Where the credential is, not what it is.** It stopped being true that the mesh
// cannot issue one when 021 was fixed, and a comment asserting a fact about the mesh that
// has become false is worse than none — somebody reads it and stops looking.
+68
View File
@@ -0,0 +1,68 @@
package catalogue
import (
"fmt"
"regexp"
"strings"
)
// Who a consumer is, said once by the mesh (novox/hq 04-ISSUES/023).
//
// **The provisioner used to invent this and nothing else could derive it.** It made a role called
// `mesh_<node>_<module>`, which is a reasonable name and is knowable nowhere else: not by the
// control plane, not by the binding, and above all not by the consumer — which has to present it
// in order to authenticate. The one identifier needed to connect was the one thing no part of the
// mesh would say.
//
// So the mesh says it. It goes to the provider in the grant and to the consumer in its binding,
// from **one derivation**, which is what makes the two ends agree by construction rather than by
// two conventions that were the same on the day they were written.
//
// **It is still name-agnostic.** The mesh does not know what a role or an access key or a client
// is; it says who is asking, and each provisioner makes that true in whatever its own system
// calls an identity. What a provider does with it is the provider's business, as everything about
// a provision is.
// identityUnusable is every character that is not safe unquoted in the systems these names reach.
//
// Conservative on purpose: lower-case letters, digits and underscore reach a PostgreSQL role, a
// MinIO access key, an LDAP uid and a Keycloak client without quoting or escaping in any of them.
// A wider set would work in most and fail in one, discovered as a login that cannot be created.
var identityUnusable = regexp.MustCompile(`[^a-z0-9_]+`)
// IdentityPrefix marks what the mesh made, so a provisioner can find its own work and leave
// everything else alone. Withdrawal depends on it entirely.
const IdentityPrefix = "mesh_"
// ConsumerIdentity is what one module on one machine is called, wherever it authenticates.
//
// A dot and a dash both become an underscore, so `home-server` and `home.server` would collide —
// which cannot happen, because a machine has one name and it is either.
func ConsumerIdentity(node, module string) string {
clean := func(s string) string {
return strings.Trim(identityUnusable.ReplaceAllString(strings.ToLower(s), "_"), "_")
}
return IdentityPrefix + clean(node) + "_" + clean(module)
}
// identityLimit is the shortest identifier limit among the systems these names reach:
// PostgreSQL's NAMEDATALEN - 1.
const identityLimit = 63
// CheckIdentity refuses a name a provider would silently shorten.
//
// **Truncation is not an error in PostgreSQL** — a name past the limit is cut to fit and the
// statement succeeds. Two consumers agreeing for the first 63 bytes would become one login, which
// is 022 again at a length nobody would think to test. Refused here rather than in each
// provisioner, because the mesh chose the name and is the only thing that can choose another.
func CheckIdentity(node, module string) error {
got := ConsumerIdentity(node, module)
if len(got) <= identityLimit {
return nil
}
return fmt.Errorf(
"%s on %s would be identified as %q, which is %d characters and some providers keep %d — "+
"another consumer shortened to the same name would share its login. Shorten the "+
"machine's name or the module's",
module, node, got, len(got), identityLimit)
}