Reconcile: adopt initialization's consolidated HQ as canonical, re-home this session's new work #24
@@ -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
|
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.
|
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
|
## What the survey found that is not about coverage
|
||||||
|
|
||||||
**Declaration and reality had drifted in the system being replaced.** Several live provisions are
|
**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
|
opened: 2026-09-01
|
||||||
located-in: [mesh-control]
|
located-in: [mesh-control]
|
||||||
fixed-by:
|
fixed-by: mesh-control 0af3ea1
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -17,6 +17,20 @@ anchor has 3 modules asking for "postgres-database" and they would share one
|
|||||||
credential: gitea, keycloak, umami
|
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
|
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
|
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
|
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
|
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
|
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.
|
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.
|
||||||
Reference in New Issue
Block a user