Files
hq/02-DECISIONS/0188-a-provider-declares-what-it-derives-for-each-consumer.md
T
jschoubben 0b9fc90885 ADR 0188: a provider declares what it derives for each consumer, and the mesh tells both ends
Issue 124: a value the mesh's own rule produced reached neither end as a
statement. The object store's provisioner derived each consumer's bucket in
its own code; all three consumers transcribed the rule into their own
definitions, one of them wrong, and each of the three also named the machine
it happens to run on.

A served value may now name the consumer the mesh is serving. Design 27
amended; issue 124 resolved.
2026-10-02 21:24:31 +02:00

8.6 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
the mesh accepted 2026-10-02 jochen false 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md

188. A provider declares what it derives for each consumer, and the mesh tells both ends

Context

An arrangement between a consumer and a provider is delivered entirely by the mesh. Where the provider is, which port it answers on, what name the consumer must present, where its password is — each arrives as a fact the consumer reads from its binding, or as ${bound:…} filled into a file before the declaration leaves the control plane. The provider invents none of it and hands none of it back (ADR 0048).

One kind of value escapes that. Where the provider names the resource — a bucket, a database, a vhost — the name is derived from the consumer, per consumer, and the mesh has no way to carry it. serves is a literal block in the provider's definition: the same values for every consumer. A provisioner's contract takes a provision and returns nothing. So a value the mesh's own rule produced reaches neither end as a statement; it is recomputed at one end and transcribed at the other.

The object store is the instance (issue 124). Its provisioner normalises the login the mesh minted into a bucket name and creates, checks and removes exactly that; the rule lives in twenty lines of the module's own TypeScript. Its three consumers each write the answer into their own definition by hand. Two transcribed it correctly; one named a predecessor's bucket, and would have authenticated successfully and been refused on every object, which reads like a credential fault and is not one.

Even corrected, the transcriptions are wrong in a second way. Each is mesh-<node>-<slug>, so each names the machine the module happens to run on today — a definition stating a fact about one installation, which ADR 0155 forbids and whose check does not catch because the name is not a domain. Move any of the three to another machine and its configuration points at a bucket its key cannot open.

The shape is not the object store's. A database provisioner that prefixed names, a queue provider that scoped vhosts, any provider that derives a resource from who is asking: each forces the consumer to reproduce somebody else's rule and keep it in agreement by hand.

Decision

1. A served value may name the consumer the mesh is serving. A serves block, which is literal today, may interpolate the mesh's own statement of who the consumer is:

  • ${consumer:as} — the identity the mesh minted for this consumer, exactly as the login it is told to present (ADR 0049);
  • ${consumer:as:dns} — the same identity written as a DNS label.

Nothing else. The mesh learns no protocol here; it spells its own name in an alphabet it already knows. The identity is the mesh's, minted by the mesh, already capped at twenty characters because of what an S3 access key accepts; dns is that same name with its separator written - instead of _, which is the whole of the difference between the mesh's identifier alphabet and the one buckets, vhosts and hostnames use. A provider that needs a prefix or a suffix writes it around the placeholder, because a served value is a string.

The rejected alternative is the provider returning values from provisioning — the natural channel, since the provider is what derived them. It is rejected for three reasons, in order of weight. It inverts the delivery the mesh is built on: a grant would carry data the provider wrote rather than only data the mesh minted, and ADR 0048 removed exactly that second path once already. It makes a consumer's declaration incomplete until its provider's reconcile loop has run, so a consumer could not be composed before a provider answered — a bootstrap order the mesh does not have and does not want. And it puts the rule where nothing can check it: a value that arrives from a running process cannot be refused at resolution, only discovered wrong later, which is the failure this record exists to end.

2. The mesh resolves it once, per consumer, and tells both ends from the one resolution. At the moment a consumer's declaration is composed, the mesh knows exactly who the consumer is. There, and only there, the placeholders are filled. The result reaches:

  • the consumer, as the served facts in its binding file and as ${bound:<provision>:<key>} in any file it writes — unchanged mechanisms, carrying one more key;
  • the provider, as serves on that consumer's entry in its contributions file, so the provisioner is told the name rather than recomputing it.

The provider stops deriving in code and starts declaring. One statement, filled once, delivered to both ends: the two cannot disagree, because there is no second computation to disagree with.

3. A served value stays settled before it is per-consumer. Settings still compose into serves (ADR 0174), and the consumer placeholders are filled after that, so an operator may set a prefix and the mesh still derives the rest. A ${consumer:…} naming a fact or an alphabet the mesh does not have is refused when the definition is parsed, with what it may say.

4. A consumer may no longer name the resource its provider derives. With the value delivered, a literal in a consumer's definition is not merely redundant — it is the one thing that can disagree with what the provider will actually create. The three object-store consumers lose their hand-written bucket names in this change.

Consequences

  • One more thing a definition may say, and one less thing a module may be wrong about. The vocabulary grows by a placeholder; the catalogue loses three literals that named this installation's control node.
  • A provider's naming rule becomes readable in its definition instead of in its source. minio's bucketFor goes; the manifest says "bucket": "${consumer:as:dns}" and the provisioner uses what it is given.
  • A provider that already serves consumers keeps serving them: the derived value equals what the code derived, so no bucket, database or login changes name. This is a change of who says it, not of what is said.
  • The mesh now holds a rule in another system's alphabet — one rule, dns, stated once. A second alphabet is a decision, not an addition: the cost of each is that the mesh must be right about somebody else's naming, and that cost is only worth paying where the mesh already mints the name.

How this is checked

  • A served value naming an unknown fact or alphabet is refused at parse, with the list of what it may say — tested on both halves of the message.
  • Resolving a consumer whose provider derives a value puts that value in the consumer's binding file, in its ${bound:…} substitutions, and in the provider's contributions entry for that consumer — one test asserting the three agree, because agreeing is the whole point.
  • Two consumers of one provider on one machine get two different derived values, and neither gets the other's.
  • A catalogue-wide test refuses a consumer definition that writes a literal where its provider derives: the provider's serves names the key, so the catalogue can say which definitions transcribe one.
  • dns is checked against the identity the mesh actually mints, not against an invented string: the test derives an identity with ConsumerIdentity and asserts the label it becomes.

References