Files
hq/04-ISSUES/023-a-consumer-cannot-build-a-connection-string/00-report.md
T
jschoubben 36d9b38a0d An issue is open, diagnosing, located, resolved or wontfix — nothing else
The playbook, the README and the status skill knew five statuses; the cycle check
knew a sixth, 'fixed', and not 'wontfix'. Eleven issues sat in the sixth for weeks
with their fixes shipped, one step short of closed. They are resolved; the check
refuses the word from now on and accepts the one the playbook allows.
2026-09-21 17:38:17 +02:00

112 lines
5.8 KiB
Markdown

---
status: resolved
opened: 2026-09-01
located-in: [mesh-control]
fixed-by: mesh-control 122680b
amended-design:
---
# 023 — A consumer is given every part of a connection except the two it cannot invent
## Symptom
A module that requires a database is now given its password in whatever shape its configuration
needs ([`022`](../022-one-credential-per-node-per-provision-not-per-module/00-report.md) and the
sealed-placeholder work). It still cannot connect, because a password is not a connection.
What it is given is a **binding**, as JSON:
```
provision postgres-database
from the node providing it
at that node's address on the private network
serves what the provider said a consumer must know — the port
```
What it needs, to write `KC_DB_URL` or `GITEA__database__USER`, is the host, the port, the
database name and **the user name**. Two of those are missing, for two different reasons.
## The user name is nobody's to say
The provisioner invents it — `mesh_<node>_<module>` — and nothing else in the mesh knows that
string. The control plane does not record it, the binding does not carry it, and the consumer
cannot derive it without hard-coding another module's naming convention.
So the one identifier a consumer must present in order to authenticate is the one thing no part
of the mesh will tell it. It works today only because nothing has yet had to write a connection
string; every proof so far stopped at "the credential arrived".
## The values cannot reach the file that needs them
The binding is a JSON document. The consumers are containers reading `KEY=value`, or a program
reading a YAML file, or one reading an attribute inside a different JSON document. A sealed secret
can now be placed inside any of those — the module writes the file with a hole in it and the host
fills the hole on the machine. **The bound values have no such route**, so the half of the
connection that is not secret is the half that cannot be delivered.
This asymmetry is backwards. The secret is the hard case, because the mesh must not be able to
read it. The host and port are ordinary facts the mesh knows in the clear, and they are the ones
stuck in a document nothing can read.
## Why it was not noticed
Every provider so far has been reached by a **provisioner**, a program written for the job, which
reads the JSON because it was built to. The first consumers to need a plain configuration file
were the first real applications. The binding was designed for the program and then handed to the
application.
There is also a stale comment saying a binding *"carries no credential: the mesh has no way to
issue one yet"*. That stopped being true when [`021`](../021-a-consumer-on-the-providers-machine-is-given-no-credential/00-report.md)
was fixed.
## What a fix has to settle
- **Who names the role.** Either the mesh records what the provisioner will create, or the
consumer contributes the name it wants and the provisioner uses it. The second is more in
keeping with the rest — a consumer already contributes the database name it wants — and it
removes an invented convention rather than documenting one.
- **How a bound value reaches a file.** The symmetric answer to the sealed placeholder, and
simpler: these values are not secret, so the control plane can put them in before sending and
the host learns nothing new.
- **That it stays name-agnostic.** The control plane must not learn what a `postgres-database`
is. What the keys mean is agreed by the requirement's name
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)), so
whatever is added has to work for a bucket and a mail relay without being told about either.
## Blocked on this
Keycloak, Gitea, Mailu and MinIO all have manifests that parse and resolve, and none of them can
start. This is what stands between the module set and a running one.
## Fixed
Both halves had the same cause: **the mesh knew something and did not say it.**
**Who a consumer is, said once.** `ConsumerIdentity(node, module)` is one derivation, sent to the
provider in its grant and to the consumer in its binding — so the two agree by construction rather
than by two conventions that happened to match on the day they were written. The provisioners now
use the name they are given and **refuse to invent one** if the mesh says nothing: falling back to
a name of their own would create a login the consumer could never guess, and everything would
report success. They also refuse a name that does not carry the mesh's prefix, because that prefix
is how withdrawal finds what it made.
**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. `at`, `as` and `from` are
true of any provision; every other key comes from what the provider said it *serves*, so the
control plane still learns nothing about what a `postgres-database` is.
Keycloak and Gitea now produce complete connection strings — asserted from the manifests on disk,
checking that every part is filled, that no placeholder survives as a value, and that the password
is still a hole only the host can close.
### Also found, and separate
The lab run that was meant to prove this failed in a way that looked like the fix being wrong: a
rotation test could not authenticate against a real database. The cause was that the suite
rebuilt the control plane's image and not the provisioner's, so a run with an image built that
minute used a provisioner built the day before. Fixed in `mesh-lab`; it is
[`005`](../005-pipeline-test-harness-unbuildable/00-report.md)'s family — a rebuild covering
most of what a run uses is worse than one covering none, because the run that follows it is
believed.