Naming which provider is only half of how a module gets a database. The other half: a module may carry its own version/fork-pinned instance, module-network only, no published port, not a provision — and the model has no word for it. Add it as a second axis with its own open questions. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
114 lines
6.6 KiB
Markdown
114 lines
6.6 KiB
Markdown
---
|
|
status: open
|
|
opened: 2026-09-20
|
|
located-in: []
|
|
fixed-by:
|
|
amended-design:
|
|
---
|
|
|
|
# A provision cannot name which provider serves it
|
|
|
|
## Symptom, as observed
|
|
|
|
The mesh models every provision — `postgres-database`, `s3-bucket`, `amqp`, a
|
|
future `oidc` — as **mesh-scoped**: there is one provider of a given kind for the
|
|
whole mesh, and a consumer that requires the provision is bound to *that* one.
|
|
`scope: "mesh"` is written into the provision definitions, and the adopted store
|
|
is a single `mesh-store`.
|
|
|
|
The mesh being migrated onto is not shaped that way, and never was meant to be.
|
|
**Node-specific services delivered to the mesh was the plan from the start.** Each
|
|
node already runs its own provider of the same kinds:
|
|
|
|
- Both control-capable nodes run their **own general-purpose postgres server**
|
|
(the same image, one per node), serving that node's own applications.
|
|
- Each node runs its **own** SQL server, its **own** redis, its **own** object
|
|
store — infrastructure is per node, by design, not a single mesh-wide instance.
|
|
- Identity is the *only* provision that is currently single (one realm on one
|
|
node), and even that already serves applications hosted on a **second** node.
|
|
|
|
So the multi-provider reality is not a future edge case that appears "the day a
|
|
second provider is added" — it is the founding topology, true today, on every
|
|
provision kind. What is missing is any way to **say it**. A consumer requires
|
|
`postgres-database`; it cannot require *this node's* postgres rather than *that
|
|
node's*. It requires `oidc`; it cannot name which node's identity provider. The
|
|
mesh model collapses a deliberately per-node fleet down to one mesh-scoped
|
|
provider, and a consumer has no field in which to choose.
|
|
|
|
## Why it matters beyond this instance
|
|
|
|
- **The model regressed an intended topology, it did not merely miss a corner
|
|
case.** "Node-specific services delivered to the mesh" is the design; `scope:
|
|
"mesh"` with a single `mesh-store` expresses the opposite. This is a gap between
|
|
a stated intent and what the manifests can represent, which is exactly what the
|
|
issues process is for.
|
|
- **It is not an identity special case.** The missing concept — *a provision has a
|
|
provider, providers are per-node, and a consumer names the provider* — is the
|
|
same for databases, object stores, brokers and identity. A fix aimed only at SSO
|
|
would leave the same wall standing behind postgres and minio, both of which are
|
|
*already* multi-provider on the live mesh.
|
|
- **The single-provider assumption is silent.** Nothing rejects a second provider
|
|
of a mesh-scoped provision; the model just cannot address it, so a consumer binds
|
|
to whichever one is "the" provider — by accident of there being one, or by a race
|
|
when there are two. A rule enforced by nothing ("there is one provider per
|
|
provision") reads as true until the second node's provider makes it false, with
|
|
no diagnostic at the seam.
|
|
- **It blocks the migration concretely.** The mesh already has applications on one
|
|
node depending on another node's provider (identity today; databases the moment
|
|
an app is assigned to a node whose local postgres is not "the" mesh store).
|
|
Modelled as mesh-scoped, that topology is expressible only by accident. To carry
|
|
it deliberately the provision must be able to name its provider.
|
|
|
|
## A second axis: provisioned against a provider, or embedded and private
|
|
|
|
Naming *which* provider is only half of "how a module gets a database". There is a
|
|
second, distinct case the model also cannot express: a module that does **not**
|
|
consume a shared provider at all, but carries its **own** instance inside its own
|
|
composition — on its own module network, publishing no host port, visible to
|
|
nothing else in the mesh. This is legitimate and sometimes necessary: some
|
|
containers pin a database *server* version or need a fork or extension set (a
|
|
customised postgres, a vector extension) that the node's shared provider does not
|
|
offer, so they must run their own alongside the main container.
|
|
|
|
The distinction that matters:
|
|
|
|
- **Provisioned** — the module requires a provision and is bound to a *named
|
|
node-scoped provider* (the first axis above). This should be the default; on the
|
|
mesh being migrated onto, most per-module databases are vanilla servers on old
|
|
version pins that could simply be consolidated onto the node's shared provider.
|
|
- **Embedded and private** — the module ships its own instance because a fork or
|
|
version genuinely forces it. Its credential is still a mesh-generated secret, not
|
|
a module-authored password; but the *instance* is module-internal — same module
|
|
network only, no published port, **not registered as a provision**, so nothing
|
|
else can bind to it and it cannot collide on a well-known port.
|
|
|
|
The model has no word for the second case. A module that carries a private instance
|
|
looks, to the mesh, either like nothing (an undeclared container) or like a provider
|
|
it must not be treated as. "Embed only when a fork or version forces it; otherwise
|
|
provision against the node's provider" is the rule the design should be able to
|
|
state and check — and the invisibility of an embedded instance (own network, no host
|
|
port, not a provision) should be an enforceable property, not a convention.
|
|
|
|
## Open questions
|
|
|
|
- Where does the provider name live — on the provision definition (`scope: "node"`
|
|
with a provider identity), on the requirement in the consumer's manifest, or
|
|
supplied only at assignment time so the same module can be bound to different
|
|
providers on different assignments?
|
|
- Is "mesh-scoped" still a legitimate scope for some provisions (a single mesh CA,
|
|
say), or does every provision become node-scoped, with a single instance
|
|
expressed as "there happens to be one"?
|
|
- What is the default when a consumer names no provider — bind to the node the
|
|
consumer is assigned to (co-located provider), require the name always, or fall
|
|
back to a mesh-wide default provider where one is declared?
|
|
- How does a provider's identity survive being moved between nodes, so a consumer's
|
|
recorded choice does not silently rebind when the provider relocates?
|
|
- Does this interact with secret rotation (the issue-scope of `rotate`) — must
|
|
rotation address a specific provider's credential holders rather than "the
|
|
provision's"?
|
|
- When a module carries an **embedded, private** instance rather than consuming a
|
|
provider, how is that declared so the mesh knows it is module-internal — not a
|
|
provision, not published, not bindable by anything else — and can enforce it?
|
|
- What decides embed-vs-provision — is it the module's declaration alone, or may an
|
|
operator override at assignment (consolidate this one onto the node's provider)?
|