Reconcile: adopt initialization's consolidated HQ as canonical, re-home this session's new work #24

Merged
jschoubben merged 177 commits from reconcile-init-into-main into main 2026-09-05 10:27:11 +00:00
3 changed files with 135 additions and 2 deletions
Showing only changes of commit 80f18caf03 - Show all commits
+22
View File
@@ -104,6 +104,28 @@ what kind it is. Small, self-contained, and only interesting once something pres
Modules published to a registry and consumed as libraries. This is a *build* output the mesh does
not deliver to a node, so it may not belong here at all.
## What checking the coverage found
Two faults, both surfaced by asking what a *real* node looks like rather than what a test does.
Neither is about the vocabulary; both are about the machinery under it.
**A credential belonged to a machine, not to a module**
([`022`](../../04-ISSUES/022-one-credential-per-node-per-provision-not-per-module/00-report.md),
fixed). A node running three services against one database could not be planned at all — and on
the consumer's side did not refuse, it just gave two of the three no credential. Every scenario
written to date had one consumer per node, which is the natural shape of a small test and not the
shape of a machine.
**A consumer still cannot build a connection string**
([`023`](../../04-ISSUES/023-a-consumer-cannot-build-a-connection-string/00-report.md), open). It
is given its own password in whatever shape its configuration needs, and the host, port and user
name are still out of reach: the user name is invented by the provisioner and recorded nowhere,
and the bound values sit in a JSON document that an application reading `KEY=value` cannot use.
The asymmetry is worth stating, because it is backwards. **The secret is the hard case** — the
mesh must not be able to read it — and the secret is the part that now arrives. The host and port
are ordinary facts held in the clear, and they are the ones stuck.
## What the survey found that is not about coverage
**Declaration and reality had drifted in the system being replaced.** Several live provisions are
@@ -1,8 +1,8 @@
---
status: located
status: fixed
opened: 2026-09-01
located-in: [mesh-control]
fixed-by:
fixed-by: mesh-control 0af3ea1
amended-design:
---
@@ -17,6 +17,20 @@ anchor has 3 modules asking for "postgres-database" and they would share one
credential: gitea, keycloak, umami
```
**And that is only the loud half.** The consuming node does not refuse at all. Three modules
wanting one database produce **one** need:
```
modules=3 needs=1
name=postgres-database from=anchor for=gitea
```
So the first module gets a credential, the other two get no file at all, and each starts and fails
to authenticate with nothing anywhere saying why — the shape of
[`021`](../021-a-consumer-on-the-providers-machine-is-given-no-credential/00-report.md), on a
different axis. The refusal that reads like a decision is on the provider; the silence is on the
consumer.
The refusal is correct about what it says. They *would* share one credential, and sharing one is
worse than refusing — a login that opens three databases is not three credentials. But the
arrangement being refused is the ordinary one. **The node this mesh exists to take over runs
@@ -79,3 +93,21 @@ said so, because from the provisioner's side it created exactly what it was aske
This crosses the control plane, the grant file naming, and every provisioner that names something
after `Consumer`. It is not a local fix, and it is the last thing between the current state and a
node that looks like a real one.
## Fixed
Needs fan out per consuming module in one place, after the resolution walk. The credential's key
gains the consuming module, the grant file is named after both halves of the consumer, needs are
matched by provision *and* module, and the provisioners name the role and the access key after the
module rather than the machine. The refusal is gone because there is nothing left to refuse.
Existing credentials are discarded rather than backfilled: they cannot say which module they were
for, and one is remade and delivered to both ends on the next push, so it costs one rotation.
A guard was added for PostgreSQL's 63-byte identifier limit, which truncates with a notice rather
than an error — two consumers whose role names agree that far would otherwise become one login,
which is this same fault at a length nobody would think to test.
What it did **not** fix is [`023`](../023-a-consumer-cannot-build-a-connection-string/00-report.md):
a consumer now receives its own password and still cannot build a connection string, because the
user name is the provisioner's invention and the bound values cannot reach a configuration file.
@@ -0,0 +1,79 @@
---
status: located
opened: 2026-09-01
located-in: [mesh-control]
fixed-by:
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.