Reconcile: adopt initialization's consolidated HQ as canonical, re-home this session's new work #24
@@ -0,0 +1,109 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-08-31
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0009-modules-and-the-graph.md
|
||||
---
|
||||
|
||||
# 27. A provision names what the consumer is coupled to, not the role it plays
|
||||
|
||||
## Context
|
||||
|
||||
Provisions are named after roles. The catalogue and every test fixture built so far say:
|
||||
|
||||
```
|
||||
provides: database
|
||||
requires: database
|
||||
```
|
||||
|
||||
**Nothing distinguishes one engine from another.** A module requiring `database` is satisfied by
|
||||
any module providing `database`, so a module written against PostgreSQL can be matched to a
|
||||
provider of Microsoft SQL Server, resolve as satisfied, deploy, and fail on its first query.
|
||||
|
||||
**The mesh runs several engines** — PostgreSQL, Microsoft SQL Server, MariaDB, and others behind
|
||||
products that expose their own. This is not a hypothetical collision.
|
||||
|
||||
**The failure is in the direction that hides.** Resolution *succeeds*. Nothing is refused, nothing
|
||||
is logged, and the breakage surfaces later as an error inside an application, on a machine, with
|
||||
nothing connecting it back to a match made elsewhere by something that thought it had done its
|
||||
job. **A wrong answer delivered confidently costs more than a refusal**, and the whole point of
|
||||
refusing on ambiguity ([ADR 0009](0009-modules-and-the-graph.md)) was to not do this.
|
||||
|
||||
**How it got in:** every test written for the resolver had exactly one provider of each name, so no
|
||||
mismatch was expressible and none was caught. The fixtures agreed with the design. That is the same
|
||||
fault as [`04-ISSUES/005`](../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md)'s
|
||||
imagined output and [`019`](../04-ISSUES/019-a-comment-asserting-a-fact-about-a-machine/00-report.md)'s
|
||||
unchecked comment, at the level of a name rather than a line.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep role names; let the operator assign correctly.** The mesh would refuse ambiguity when two
|
||||
providers exist, so a person picks. **Rejected.** It makes correctness depend on somebody
|
||||
knowing that the module they are assigning speaks a particular dialect — which is exactly the
|
||||
knowledge the provisioning model exists to remove. And with one provider of each name, nothing
|
||||
is ambiguous and nothing is asked.
|
||||
|
||||
2. **A role name plus a `flavour:` or `engine:` qualifier**, matched as a second field.
|
||||
**Rejected.** Two fields that must agree is a constraint the resolver has to enforce and a
|
||||
manifest author has to remember, to express something one field already can. The name is the
|
||||
contract; splitting it invites a requirement that names a role and forgets the qualifier, which
|
||||
then matches everything again.
|
||||
|
||||
3. **The name says what the consumer is coupled to.** **Adopted.**
|
||||
|
||||
## Decision
|
||||
|
||||
**A provision is named for the thing a consumer's code is written against.**
|
||||
|
||||
```
|
||||
provides: postgres-database
|
||||
requires: postgres-database
|
||||
```
|
||||
|
||||
**The test is whether the consumer can tell the difference.** If swapping the provider would break
|
||||
the consumer, the name must say which provider — because a match that breaks the consumer is not a
|
||||
match. If the consumer genuinely cannot tell, a role name is correct and better.
|
||||
|
||||
| provision | | why |
|
||||
|---|---|---|
|
||||
| `postgres-database`, `mssql-database` | **specific** | applications are written against a dialect; a swap breaks them |
|
||||
| `route` | **role** | the consumer wants its name reachable and does not care what proxies it |
|
||||
| `resolver` | **role** | the consumer wants names to resolve |
|
||||
| `artifact-store` | **role** | the consumer fetches by digest over a protocol, and nothing else |
|
||||
|
||||
**`database` is not a provision and may not be provided.** There is no context in which an
|
||||
application talks to a generic database: it talks to PostgreSQL or it talks to SQL Server. A name
|
||||
that cannot be true of any real consumer should not be expressible.
|
||||
|
||||
**This is about coupling, not about products.** Two providers of `postgres-database` — a container
|
||||
on this node and a managed instance elsewhere — are interchangeable and *should* both match. What
|
||||
may not be interchangeable is what the consumer's queries are written in.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Every manifest that names a database changes.** Doing this now costs a rename across a handful of
|
||||
examples. Doing it after modules are migrated costs it across all of them, plus every deployment
|
||||
that resolved against the old name.
|
||||
|
||||
**Wrong requirements now fail loudly, and at the right moment.** A module requiring
|
||||
`postgres-database` where only `mssql-database` is provided is unsatisfiable, so it is **refused at
|
||||
resolution** with both names visible — rather than deployed and broken later. This is the property
|
||||
that was lost, restored.
|
||||
|
||||
**Generic role names are still right, and the rule says when.** This does not push specificity
|
||||
everywhere; it puts it exactly where a consumer is coupled. Naming `route` after a particular proxy
|
||||
would be the same error in the other direction, and would prevent a swap that genuinely changes
|
||||
nothing.
|
||||
|
||||
**It is checked, not merely stated** ([`00-META/how-we-build.md`](../00-META/how-we-build.md) §5).
|
||||
A manifest providing a name known to be engine-generic is refused, naming what to say instead.
|
||||
Without that, this record is a convention, and a convention is what the previous naming was.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0009](0009-modules-and-the-graph.md) — provisions, and refusing on ambiguity
|
||||
- [`03-DESIGN/01-to-be/07-the-substrate.md`](../03-DESIGN/01-to-be/07-the-substrate.md) — *the
|
||||
provisioning model uses databases, roles and schemas as PostgreSQL means them*, which is this
|
||||
record's point made about the substrate before it was made about modules
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
topic: the tiers
|
||||
status: accepted
|
||||
date: 2026-08-31
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
|
||||
---
|
||||
|
||||
# 28. The substrate supplies the control plane and nothing else
|
||||
|
||||
*Corrects one row of [ADR 0006](0006-the-substrate-and-the-control-plane.md) and makes explicit
|
||||
something it left unsaid. The rest of that record stands.*
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0006 defines the substrate by a circularity: **what the control plane needs in order to run,
|
||||
and cannot ask itself for, because it is not running yet.** Two questions, and both must be
|
||||
answered *yes* for something to be substrate.
|
||||
|
||||
Its membership table admits the object store on this line:
|
||||
|
||||
| role | product | |
|
||||
|---|---|---|
|
||||
| object store | **MinIO** | it cannot grant itself a bucket |
|
||||
|
||||
**That answers the second question and assumes the first.** It is true that a control plane cannot
|
||||
grant itself a bucket. Nothing establishes that it needs one.
|
||||
|
||||
**It does not.** Verified 2026-08-31 against `mesh-control`: no S3 client, no bucket, no object
|
||||
storage of any kind outside comments. Artifacts reach nodes as content-addressed blobs in the OCI
|
||||
registry, and the code records the decision and its reasoning:
|
||||
|
||||
> One store, and it is the registry the bootstrap already pulls from. An OCI registry is a
|
||||
> content-addressed blob store that happens to also understand images… The alternative considered
|
||||
> was a second store beside it — S3-shaped, buckets, signed URLs. It is the right answer for
|
||||
> objects that are *mutable*, or need per-reader access, or are not build output. None of that
|
||||
> describes a digest-pinned archive, and standing up a second service to hold one kind of
|
||||
> immutable blob means two things to run, two things to back up and two ways for an artifact to be
|
||||
> missing.
|
||||
|
||||
**The row is inherited from the system being replaced**, where an object store distributed module
|
||||
tarballs. Here nothing does, and the row was never re-tested against the definition it sits under.
|
||||
|
||||
**A second thing ADR 0006 never says:** whether a substrate service and a module of the same
|
||||
product are the same instance. It says the substrate is *not the control plane* and *not a place
|
||||
for logic*, and stops. The question is not idle — an application wanting a database, on a mesh
|
||||
whose substrate is already running PostgreSQL, has an obvious wrong answer available.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Leave the object store as substrate, unused.** Harmless-looking. **Rejected.** A membership
|
||||
list that includes something nothing needs is a list that has stopped being derived from its
|
||||
test, and the next member is admitted by precedent instead of argument. It also mandates that
|
||||
every mesh run a service no mesh uses.
|
||||
|
||||
2. **Applications share the substrate's instances.** One PostgreSQL, one of everything.
|
||||
**Rejected**, below.
|
||||
|
||||
3. **The substrate is exactly what the control plane consumes; everything else is a module.**
|
||||
**Adopted.**
|
||||
|
||||
## Decision
|
||||
|
||||
**The object store is not substrate.** It fails the first half of the test: the control plane does
|
||||
not need one. An object store is an ordinary module, required through the module graph like
|
||||
anything else, and a module wanting one depends on a module providing one.
|
||||
|
||||
**The substrate has four members, not five**: a relational store, a message bus, an image registry,
|
||||
and conditionally an identity provider. The registry stays — the control plane genuinely cannot
|
||||
deliver an artifact without somewhere to put it.
|
||||
|
||||
**A substrate service and a module of the same product are different instances, and are not
|
||||
shared.** The mesh's own PostgreSQL and a PostgreSQL a workload was given are two servers, two
|
||||
containers, two lifecycles.
|
||||
|
||||
Three reasons, and the first is the one that matters:
|
||||
|
||||
**The substrate is not in the module graph.** It is raised from the pinned bundle the host carries,
|
||||
before any mesh exists to declare it. A workload depending on it would depend on something the
|
||||
graph cannot see, cannot rotate a credential for, and cannot move — which is every property the
|
||||
provisioning model exists to provide.
|
||||
|
||||
**It would put workload data in the control plane's own store.** The mesh's contexts own their
|
||||
stores exclusively ([ADR 0008](0008-a-context-owns-its-store.md)). An application sharing that
|
||||
server can exhaust it, lock it, or fill its disk, and the failure is the control plane going down
|
||||
— which is the one failure that makes every other one harder to fix.
|
||||
|
||||
**They are bounded differently.** The substrate is sized, backed up and upgraded as part of
|
||||
bootstrapping a mesh. A workload's database follows the workload — moved with it, destroyed with
|
||||
it, restored with it.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Migrating an object store is ordinary module work**, not substrate work. It was previously going
|
||||
to be done as part of completing the substrate, which would have been the wrong shape and would
|
||||
have coupled every mesh to a service the mesh does not use.
|
||||
|
||||
**A mesh with no workload needing one runs no object store at all.** That is the correct outcome
|
||||
and was not previously available.
|
||||
|
||||
**Two PostgreSQL containers on a node that hosts both is expected**, not duplication to be
|
||||
optimised away. Anyone tidying them together should find this record first.
|
||||
|
||||
**"Substrate by role and ordinary by delivery" loses one of its two members.** ADR 0006 uses that
|
||||
phrase of the object store and the registry — things that are substrate but provisioned once a
|
||||
control plane exists. It now describes the registry alone.
|
||||
|
||||
**The definition is applied, not just stated.** Both halves of the circularity test are asked of
|
||||
each member, and *cannot grant itself one* is not sufficient on its own — it is true of almost any
|
||||
service, which is what made it possible to admit a member on that half alone.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0006](0006-the-substrate-and-the-control-plane.md) — the definition, and the table this
|
||||
corrects one row of
|
||||
- [ADR 0008](0008-a-context-owns-its-store.md) — a context owns its store exclusively
|
||||
- `mesh-control internal/builder/registry.go` — where artifacts go, and why not S3
|
||||
@@ -92,6 +92,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0006** — [The substrate and the control plane](0006-the-substrate-and-the-control-plane.md)
|
||||
- **0007** — [Connectivity](0007-connectivity.md)
|
||||
- **0008** — [A context owns its store, exclusively](0008-a-context-owns-its-store.md)
|
||||
- **0028** — [The substrate supplies the control plane and nothing else](0028-the-substrate-supplies-the-control-plane-and-nothing-else.md)
|
||||
|
||||
### What runs on them, and how it gets there
|
||||
|
||||
@@ -99,6 +100,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0010** — [Delivery](0010-delivery.md)
|
||||
- **0024** — [Model access is a provision, and a licence is a thing with a name](0024-model-access-is-a-provision.md) *(proposed)*
|
||||
- **0026** — [The mesh has a session of its own, and it is the node session's mechanism](0026-the-mesh-has-a-session-of-its-own.md)
|
||||
- **0027** — [A provision names what the consumer is coupled to, not the role it plays](0027-a-provision-names-what-the-consumer-is-coupled-to.md)
|
||||
|
||||
### How it is built
|
||||
|
||||
|
||||
@@ -36,12 +36,22 @@ The test, applied:
|
||||
|---|---|---|---|
|
||||
| a relational store — **PostgreSQL** | its own state lives there | no — provisioning needs the store | **substrate** |
|
||||
| a message bus — **LavinMQ** | it reaches nodes over it ([ADR 0002](../../02-DECISIONS/0002-nodes-communicate-over-a-broker.md)) | no — it cannot grant itself a virtual host | **substrate** |
|
||||
| an object store — **MinIO** | artifacts and blobs it delivers | no — it needs a bucket to hold them | **substrate** |
|
||||
| ~~an object store~~ | ~~artifacts and blobs it delivers~~ | — | **not substrate** — [ADR 0028](../../02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md) |
|
||||
| an image registry — **the OCI registry** | images it delivers to nodes | no — it needs a repository | **substrate** |
|
||||
| an identity provider | only if it delegates authentication | — | **conditional, below** |
|
||||
| ingress — **Traefik** | not to start; only to be reached by name | — it grants itself one afterwards | **not substrate** ([ADR 0007](../../02-DECISIONS/0007-connectivity.md)) |
|
||||
| anything else the mesh hosts | no | — | not substrate |
|
||||
|
||||
*The object-store row was wrong, and how it was wrong is worth keeping.* It answered *can it
|
||||
grant itself one* — no, it cannot grant itself a bucket — while assuming the first column. **The
|
||||
control plane does not need an object store**: it has no S3 client and never has, and artifacts
|
||||
reach nodes as content-addressed blobs in the registry. The row was inherited from the system being
|
||||
replaced, where an object store distributed module tarballs, and was never re-tested against the
|
||||
definition above it. *Both columns must be answered, and the second is true of almost any service.*
|
||||
|
||||
**An object store is an ordinary module**, required through the module graph by whatever wants one.
|
||||
A mesh with no workload needing one runs none.
|
||||
|
||||
**The role and the product are both written**, here and everywhere
|
||||
([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)). The role is what the argument
|
||||
turns on — the test above works on roles, and would give the same answers for a different store.
|
||||
@@ -83,6 +93,17 @@ cannot obtain it* is.
|
||||
its own.* A substrate service is an upstream image, pinned, with configuration.
|
||||
- **Not privileged.** The substrate is provisioned *from* by the control plane and grants
|
||||
nothing on its own initiative.
|
||||
- **Not the mesh's supply of anything**
|
||||
([ADR 0028](../../02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md)).
|
||||
A substrate service and a module of the same product are **different instances**. The mesh's own
|
||||
PostgreSQL and a PostgreSQL a workload was given are two servers, and a node hosting both runs
|
||||
two containers — expected, not duplication to be tidied away.
|
||||
|
||||
The substrate is raised from the bundle before any mesh exists, so **it is not in the module
|
||||
graph**: a workload depending on it would depend on something the graph cannot see, cannot rotate
|
||||
a credential for, and cannot move. It would also put workload data in the store the control plane
|
||||
keeps its own state in, where a workload that fills a disk takes down the one thing needed to fix
|
||||
it.
|
||||
|
||||
## The pinned bundle
|
||||
|
||||
|
||||
Reference in New Issue
Block a user