Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49b1136ded |
@@ -1,63 +0,0 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller cmd/mesh-controller/modules.go (assign takes no provider; pin is a separate, per-machine command)
|
||||
- mesh-controller internal/inventory (provision_pin keyed by (node, name))
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 163 — An assignment does not record which provider answers it
|
||||
|
||||
## What was observed
|
||||
|
||||
Planning ace's modules that need a database (baserow, letta, n8n, and the apps using ace's
|
||||
predecessor postgres). The operator's model — and ADR 0110's — is that **an assignment states where
|
||||
each of its requirements is answered from**: gitea's assignment on novox says its `postgres-database`
|
||||
comes from novox; an app assigned to ace says whether its database comes from ace or from novox.
|
||||
|
||||
The mesh holds no such statement for any assignment. Read on novox (2026-09-30):
|
||||
|
||||
```
|
||||
select … from provision_pin; -- 0 rows
|
||||
```
|
||||
|
||||
Every requirement in the mesh resolves implicitly, each time, by ADR 0084's order (a pin, then the
|
||||
provider on the consumer's own node, then the only provider).
|
||||
|
||||
## What was decided, and what exists
|
||||
|
||||
[ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md):
|
||||
|
||||
> Where several remain and none is local, **a person chooses when the module is assigned**.
|
||||
> Assignment lists the candidates, with the holder of a seat that delivers the provision suggested
|
||||
> first, and records the answer on the assignment as its pin. Without an answer the module is not
|
||||
> assigned.
|
||||
|
||||
What the control plane implements:
|
||||
|
||||
| decided | implemented |
|
||||
|---|---|
|
||||
| the answer is recorded **on the assignment** | `provision_pin` is keyed `(node, name)` — one answer per machine per provision, shared by every module on it |
|
||||
| chosen **at assignment** | `assign <node> <module>` takes no provider; `pin <node> <provision> <from-node>` is a separate command |
|
||||
| an assignment may be answered from its own machine (gitea ← novox) | `pin` refuses a machine pinning to itself ("does not need saying") |
|
||||
| every assignment has an answer | none recorded; resolution guesses the same answer every time |
|
||||
|
||||
## Consequence
|
||||
|
||||
Nothing is wrong *today* — with one postgres provider, every guess is the intended answer. But the
|
||||
answer is not a fact anyone stated, so:
|
||||
|
||||
- **it changes silently** the day a second provider appears (e.g. a postgres assigned on ace): every
|
||||
unpinned consumer re-resolves — a consumer on ace moves from novox's database to an empty one on ace
|
||||
at the next push, which is data a module stops seeing without anything saying so;
|
||||
- two modules on one machine cannot take one provision from different providers;
|
||||
- a person reading an assignment cannot see where its data lives.
|
||||
|
||||
## What would be right
|
||||
|
||||
ADR 0110 as written: `assign` records, per requirement, the node that answers it (its own node
|
||||
included), offering the candidates and refusing an assignment without an answer where several exist;
|
||||
the per-machine `provision_pin` becomes a per-assignment record, with existing assignments backfilled
|
||||
from what they resolve to now so nothing moves.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/inventory/secrets.go (SecretFor mints a pair credential nobody accepted)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 164 — A credential that must be accepted is minted anyway
|
||||
|
||||
## What was observed
|
||||
|
||||
Provisioning ace's modules. Several providers hold exactly one credential they did not get from the
|
||||
mesh and cannot take one from it: a Servarr app's API key (sonarr, radarr, lidarr), jackett's API key,
|
||||
plex's X-Plex-Token, nzbget's ControlPassword, qBittorrent's WebUI password. Their consumers' pair
|
||||
credential must be **accepted** by the operator (ADR 0092). Until it is, `SecretFor` mints a random
|
||||
value, seals it to both ends, and reports nothing: the value can never work.
|
||||
|
||||
Every consumer therefore had to learn to detect it — try the credential against the provider first,
|
||||
refuse a value the provider rejects, print the `secret accept` command — six write-in steps, one probe
|
||||
each (ombi, home-assistant, and the four download-stack consumers). qBittorrent bans an address after
|
||||
five failed logins, so a consumer retrying a minted value locks itself out.
|
||||
|
||||
## What would be right
|
||||
|
||||
A provision (or a provider's `serves`) can declare its pair credential **accepted-only**. The plan then
|
||||
refuses the pair — naming the accept command — instead of minting, and a consumer is never handed a
|
||||
value the mesh knows cannot work.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/inventory/secrets.go (AcceptSecretForPair is per consumer)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 165 — One accepted value must be accepted once per consumer
|
||||
|
||||
## What was observed
|
||||
|
||||
On ace, jackett's API key is the pair credential for sonarr, radarr, lidarr and bookshelf; sonarr's is
|
||||
the credential for ombi, bazarr and home-assistant. It is **one value**, owned by the provider — yet
|
||||
`secret accept` is per pair, so ace's download stack alone needs 12 accepts of 3 values, and rotating
|
||||
a provider's key means finding and re-accepting every pair. Missing one leaves that consumer on a
|
||||
stale (or minted, 164) value.
|
||||
|
||||
## What would be right
|
||||
|
||||
A provider-level accept: "this provider's credential for `<provision>` is X" — delivered to every
|
||||
consumer pair, current and future, and rotated in one place. Pairs whose credential is genuinely per
|
||||
consumer (postgres, keycloak, mosquitto, influxdb — minted and created by a provisioner) are unaffected.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/catalogue (requires is a list of hard requirements)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 166 — A requirement cannot be optional
|
||||
|
||||
## What was observed
|
||||
|
||||
Making every dependency on ace a provision turned soft dependencies into hard ones. grafana now
|
||||
requires `influxdb-api` (a data source), ombi requires `sonarr-api`, `radarr-api` and `lidarr-api`,
|
||||
home-assistant requires the Servarr APIs and `mqtt-topic`. Each is optional to the software — grafana
|
||||
runs without a data source, ombi without lidarr — but a mesh without influxdb cannot assign grafana at
|
||||
all, and a mesh without lidarr cannot run ombi.
|
||||
|
||||
## What would be right
|
||||
|
||||
A requirement a module can run without: resolved and bound when a provider exists, absent (with its
|
||||
`${bound:…}` placeholders refused or defaulted explicitly, never rendered empty) when none does — so
|
||||
the module description stays true on every mesh.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-catalog (each module builds from its own directory, ADR 0069)
|
||||
- mesh-sdk
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 167 — Code several modules share has no home
|
||||
|
||||
## What was observed
|
||||
|
||||
The download-stack write-in step (register download clients and torznab indexers through the Servarr
|
||||
API) is identical for sonarr, radarr, lidarr and bookshelf. Because a module builds from its own
|
||||
directory, it now exists as four byte-identical copies under `modules/<m>/downloads/`, kept honest by a
|
||||
test that fails when one differs. The same shape repeats: an MQTT probe copied into two modules, and a
|
||||
"write the provider into the app through its API, idempotently, refuse a minted value" step in ombi,
|
||||
home-assistant, nodered, tautulli and the four downloaders.
|
||||
|
||||
## What would be right
|
||||
|
||||
A home for shared module code the builder can use — an sdk helper (a write-in step harness: read
|
||||
bindings and pair credentials, probe the provider, diff, write, report) or a shared package the
|
||||
catalogue builds once — so a fix lands in one place.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/catalogue/settings.go (settle: every key but `ports` merges into every mergeable file and every contribution)
|
||||
- mesh-controller internal/catalogue/declaration.go (a provider's settings are laid over what it serves)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 168 — A setting reaches every file and every contribution
|
||||
|
||||
## What was observed
|
||||
|
||||
Settings merge key by key into **every** `"merge": "json"` file of a module **and** every contribution
|
||||
it makes; a provider's settings are also laid over what it serves. Seen on ace:
|
||||
|
||||
- searxng's `endpoints` and a route `label` land in searxng's own `settings.yml`; nodered's
|
||||
`timeZone` and `mqtt` keys land in mosquitto's grants file; keycloak's `issuer` lands in its
|
||||
`postgres-database` and `route` contributions.
|
||||
- every consumer's `plex-api` binding carries plex's `endpoints` and `expose` settings — and a provider
|
||||
setting named `port` would silently redirect every consumer.
|
||||
- a module cannot have two configurable files: searxng's sidecar config had to stop being mergeable
|
||||
so searxng's keys would not reach it.
|
||||
|
||||
Harmless today only because every receiver happens to ignore unknown keys.
|
||||
|
||||
## What would be right
|
||||
|
||||
A setting is aimed: at a file (by resource id), at a contribution (by requirement), or at what the
|
||||
module serves — declared settable by the module (ADR 0046 already says settings drive "the fields the
|
||||
manifest marks") — and an unaimed key is refused like any unknown setting.
|
||||
Reference in New Issue
Block a user