diff --git a/02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md b/02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md index 4194c23..de92a89 100644 --- a/02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md +++ b/02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md @@ -108,6 +108,14 @@ declared slug is a strictly better escape hatch than an opaque hash. **D stays r ## Consequences (of E) +> **The mechanism changed — 2026-10-06, by [ADR 0225](0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md).** +> Option C, named above as the later refinement, is taken: each offer states the longest identity its +> backend keeps, and a consumer is bounded by the provision it requires rather than by 20 everywhere. +> 20 stays the bound of the object store and of a provider that is told its consumers and does not +> say. The overflow is refused before merge by the catalogue check, and at composition the consumer +> is left out of its provider's grants and reported — never the provider's machine refused. What +> stands: the identity is said once, the slug is the remedy, nothing is hashed or truncated. + - A module manifest gains an optional `slug`; a node may carry one too. `ConsumerIdentity` prefers the slug over the cleaned name for each half. `identityLimit` becomes 20 (the true minimum), and `CheckIdentity` refuses at `module add` / assignment — now with a message naming the slug to set. diff --git a/02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md b/02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md index 9d1b1a3..b6214ec 100644 --- a/02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md +++ b/02-DECISIONS/0202-a-provider-declares-what-it-derives-for-each-consumer.md @@ -64,6 +64,13 @@ instead of `_`, which is the whole of the difference between the mesh's identifi 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 mechanism changed — 2026-10-06, by [ADR 0225](0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md).** +> The identity is no longer capped at twenty characters for every provision: each offer states the +> bound its backend keeps, and twenty is the bound of the object store and of a provider that does not +> say. What stands: the identity is still the mesh's, and `dns` is still that name with its separator +> written `-`. An offer serving `${consumer:as:dns}` now bounds its consumers at 63 or less, so the +> label still fits. + 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 diff --git a/02-DECISIONS/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md b/02-DECISIONS/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md new file mode 100644 index 0000000..9661a53 --- /dev/null +++ b/02-DECISIONS/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md @@ -0,0 +1,148 @@ +--- +topic: what runs on it +status: accepted +date: 2026-10-06 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md +--- + +# 225. A consumer's identity is bounded by the provision it requires, judged before merge, and never refuses its provider + +## Context + +[ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md) bounds every consumer's identity, +`mesh__`, by the tightest backend anywhere in the mesh: an S3 access key's 20 +characters. It chose that over per-interface bounds (its option C) because one constant unblocked the +object store, and named C as the refinement "if non-S3 consumers are paying for S3's limit often +enough to mind". [Issue 263](../04-ISSUES/263-every-consumer-pays-for-the-tightest-backends-name-limit/00-report.md) +is that point. The operator: "the 20 character limit has bitten us multiple times". + +What happened on 2026-10-06, and what it shows: + +- **The bound applied where it meant nothing.** A change made the network-manager modules require the + mesh's resolver provision. The resolver mints no credential and keeps no name: its provider is not + even told who its consumers are (it receives nothing). `networkmanager`'s identity, 23 to 26 + characters on real machine names, was refused all the same. +- **It was found late and far from its cause.** The catalogue's module check and the controller's + tests passed. ADR 0049 says the refusal comes at assignment; this was a new requirement on modules + already assigned, so no assignment saw it. It surfaced when the provider composed its grants. +- **It refused the wrong machine, wholly.** The controller judged the identity while composing the + *provider's* declaration, and one refusal there fails the whole composition. The anchor holds the + resolver, so the anchor — every module on it — could not be pushed until a slug was changed + elsewhere. + +The catalogue's providers keep very different names. Read from each provider's code: the object store +keeps the identity as an access key (20) and a bucket name; PostgreSQL as a role and a database (63); +MongoDB as a database (63); SQL Server as a login and a database (128); the identity provider as a +client id (255); the forge as a user name (40); the mail server as a mailbox's local part (64); the +public DNS provider as a label (63); the cache, the vault, the message broker and the time-series +store as names with no limit worth stating. The resolver and both route providers keep no name of +their consumers at all. + +## Considered Options + +1. **Keep one bound, raise or lower it.** Any single number is wrong for most provisions: 20 refuses + a database consumer for an object store's key, 63 lets the object store fail at provision time + again ([issue 034](../04-ISSUES/034-mesh-login-exceeds-s3-access-key-limit/00-report.md)). Rejected — + it is the cause. +2. **Per-interface bounds in a mesh-wide table of provisions.** Puts the fact in the controller, which + would then know what an S3 key is. The mesh is name-agnostic about what a provider does with an + identity (`ConsumerIdentity`); a table would make it learn every backend. Rejected. +3. **Each offer states its own bound; the bound a consumer meets is that of the provision it + requires** (ADR 0049's option C, placed where [ADR 0202](0202-a-provider-declares-what-it-derives-for-each-consumer.md) + already places what a provider derives: in its own definition). Chosen. +4. **Derive a different identity per provision** (C's own "against": one consumer, several names). + Not needed: the identity stays one name, said once, and must fit every provision the module + requires. Only the *bound* is per provision. Rejected as unnecessary. + +And for where the refusal lands: + +5. **Keep refusing the provider's composition.** Rejected — it makes one consumer's name a reason + no push reaches a machine that did nothing wrong. +6. **Refuse the consumer's whole machine.** Right machine, still too wide, and still late. Not chosen; + the consumer is named instead, and refused in its own pull request (below). + +## Decision + +**1. An offer states the longest consumer identity its backend keeps.** In the provider's +definition, beside the provision: `identity` with a `max` and the words for what keeps it (`in`), so a +refusal can say "an S3 access key keeps 20"; `in` alone for a backend with no limit worth stating; or +`false` for a provision that keeps no name derived from its consumer. The bound applied to a consumer +is that of the provision it requires, from the offer of the module answering it — not the tightest +backend in the mesh. + +**Unsaid, the bound follows from what the provider is told.** A provider that receives the provision, +or serves its consumers a value built from their identity, is told who each consumer is and may make +a name of it in a backend nobody measured: it keeps ADR 0049's 20. A provider told neither keeps +nothing of its consumers: no bound. A provider whose definition is not at hand is held to 20. + +**2. An overflow is refused before merge.** The catalogue check (`module check`, and the controller's +test over the real catalogue) judges every module's identity, built on the longest machine name of +the mesh, against the bound of every provision it wants that a module in the catalogue offers — the +tightest where several offer it. The pull request that introduces an overflow — a new requirement, a +lowered bound, a longer module name — is the one that fails, naming the module and the longest slug +that would fit. The longest machine name is a parameter of the check; its default is the mesh's own +longest, raised in the same change that names a longer machine. + +**3. One consumer's identity never refuses its provider's machine.** When the provider's declaration +is composed, a consumer whose identity overflows the bound is left out of the grants and returned +beside them; every other consumer is granted and the declaration composes. The consumer is named +where an operator looks: on the push and plan of the provider, on the plan of the consumer's own +machine, and in `status` (and its document), which does not call the mesh well while one stands. + +**4. An identity served as a DNS label stays inside one.** The mesh writes an identity into a label +without truncating it ([ADR 0202](0202-a-provider-declares-what-it-derives-for-each-consumer.md)), +so an offer that serves `${consumer:as:dns}` must bound its consumers at 63 or less. + +What does not change: the identity is still derived once and said to both ends (ADR 0049, issue 023); +the slug is still the remedy; truncation and hashing are still refused. + +## Consequences + +- A consumer of a database or the identity provider keeps a legible name on a long machine name; only + a consumer of the object store still needs a slug of a few characters. Requiring a keyless + provision costs nothing in name length. +- The catalogue's providers each state a bound in their offer, and the check reads it there. A new + provider that is told its consumers and says nothing keeps the old 20: safe, and visible in review. +- A consumer left out of the grants holds a binding and credential its provider never created. It + fails to authenticate; that is reported by name in `status` until a slug fixes it, rather than + hidden behind a machine nobody could push. +- The check's default machine-name length is a fact about one mesh carried in code. A mesh that + names a longer machine must raise it, or pass its own to `module check`; nothing reminds it to. +- Old controllers refuse the new field (manifests are parsed strictly), so the controller ships before + the catalogue that states bounds. +- ADR 0049's consequence "`identityLimit` becomes 20 … `CheckIdentity` refuses at `module add` / + assignment" now holds only for a provision that keeps 20, and is checked before merge and reported + at composition rather than refused at assignment. ADR 0049 carries a note saying so. + +**How each rule is checked.** + +- *Rule 1:* catalogue unit tests — an offer's stated bound is the one applied; an unstated one is 20 + for a provider told its consumers and none for one told nothing; `false` is none; the field parses + strictly and a bound without `in`, or shorter than any identity, is refused when the definition is + parsed. Over the real catalogue: the resolver provision bounds nothing and the object store 20. +- *Rule 2:* `module check` refuses a module whose identity overflows what it requires, naming the + slug length, and passes a long name requiring a keyless provision; a test runs the same judgement + over every manifest in the real catalogue on the default machine-name length, so the catalogue's + own pull request fails on an overflow. +- *Rule 3:* a controller test against a real store reproduces the night it was found — the network + manager, no slug, on a six-character machine, requiring the resolver provision, beside a consumer + that overflows an object store: the provider's declaration composes, the network manager is granted, + the overflowing consumer is left out, named by the push, carried by `status` and its document, and + the mesh is not called well. +- *Rule 4:* a test that a DNS-label identity under a bound over 63 is refused when the definition is + parsed. + +## References + +- [Issue 263](../04-ISSUES/263-every-consumer-pays-for-the-tightest-backends-name-limit/00-report.md) — + the observation. +- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md) — the bound this refines; its + option C. +- [ADR 0202](0202-a-provider-declares-what-it-derives-for-each-consumer.md) — + a provider declares what it derives; the bound is declared beside it. +- `mesh-controller` `internal/catalogue/identity.go` (`IdentityBound`, `CheckIdentityWithin`, + `IdentityProblems`, `Resolution.Overflowing`), `manifest.go` (`Offer.Identity`, + `IdentityBoundOf`), `cmd/mesh-controller/plan.go` (`grantsFor`), `check.go`, `status.go`. +- `mesh-catalog` — each provider's offer states its bound. diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 3a52a78..e945e5d 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -323,6 +323,7 @@ python3 00-META/checks/index.py fail if stale - **0215** — [The machine's message bus is a node seat, and it is never restarted live](0215-the-machines-message-bus-is-a-node-seat-and-is-never-restarted-live.md) - **0216** — [The agent's configuration is registered through its module, at three scopes, and served as one plugin](0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md) - **0220** — [What a machine asks needs its uplink held, and the retired resolver pieces go](0220-what-a-machine-asks-needs-its-uplink-held-and-the-retired-resolver-pieces-go.md) +- **0225** — [A consumer's identity is bounded by the provision it requires, judged before merge, and never refuses its provider](0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md) ### How it is built diff --git a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md index b5841c8..f55a9cf 100644 --- a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md +++ b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md @@ -2,8 +2,9 @@ layer: to-be status: in-progress code: [mesh-controller internal/catalogue] -updated: 2026-10-02 +updated: 2026-10-06 decisions: + - 02-DECISIONS/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md - 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md - 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md - 02-DECISIONS/0115-one-assignment-of-a-module-per-node.md @@ -250,7 +251,11 @@ value in a container's environment is refused when the definition is parsed, wit **A module is assigned at most once to a node**, and that pair is the assignment's identity ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)). Its directories, containers, login, broker account and settings are keyed by it, as today, and a login still fits the -tightest backend ([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)). +backend that keeps it ([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)): +the bound is the one the offer of each provision it requires states, a keyless provision states none, +and an overflow is refused by the catalogue check before merge and left out of the provider's grants, +reported, at composition — never a refusal of the provider's machine +([ADR 0225](../../02-DECISIONS/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md)). **A module may run on many nodes, and one assignment may hold a seat** ([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md))). The definition diff --git a/04-ISSUES/263-every-consumer-pays-for-the-tightest-backends-name-limit/00-report.md b/04-ISSUES/263-every-consumer-pays-for-the-tightest-backends-name-limit/00-report.md index 3596eee..52829c9 100644 --- a/04-ISSUES/263-every-consumer-pays-for-the-tightest-backends-name-limit/00-report.md +++ b/04-ISSUES/263-every-consumer-pays-for-the-tightest-backends-name-limit/00-report.md @@ -1,7 +1,7 @@ --- -status: open +status: located opened: 2026-10-06 -located-in: [] +located-in: [mesh-controller internal/catalogue, mesh-controller cmd/mesh-controller, mesh-catalog modules] fixed-by: amended-design: --- @@ -52,6 +52,22 @@ the remedy. That has two costs: 3. **Never refuse a provider's whole declaration** for one consumer's identity. Leave that consumer's grant out, say so in `status`, and keep the provider's machine pushable. +## Where it lives + +- **The one bound** is `identityLimit` in the controller's `internal/catalogue/identity.go`, applied to + every consumer whatever it requires. Nothing in a provider's definition could say otherwise. +- **The refusal** was raised in `grantsFor` (`cmd/mesh-controller/plan.go`), while composing the + *provider's* declaration: an error there fails the whole composition, so the anchor could not be + pushed for a module on another machine. No assignment, module check or test judged it before. +- **The facts** are in each provider's code in the catalogue: the object store keeps the identity as + an access key (20), the databases as roles and database names (63, 128), the identity provider as a + client id (255); the resolver and the route providers keep no name of their consumers. + +Decided in [ADR 0225](../../02-DECISIONS/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md), +which takes ADR 0049's option C and adds the two placements the issue asks for: each offer states its +bound, `module check` judges every identity on the longest machine name before merge, and a provider +leaves an overflowing consumer out of its grants and says so, in `status` too, instead of refusing. + ## How it is checked (once fixed) - A catalogue test: a module requiring a keyless provision composes with a long name.