Files
hq/02-DECISIONS/0164-a-setting-is-declared-with-its-default-its-meaning-and-what-changing-it-costs.md
T
jschoubben f1941304cc ADRs 0164-0166 and issue 190: the container runtime gets a module, a seat and declared settings
Proposed for the operator's review: settings declared with defaults and cost (0164),
container-runtime as a kernel capability (0165), node-container-runtime seat with the
host creating containers through its holder (0166), and the runtime's file written by
modules that are not its own (190).
2026-10-01 23:13:18 +02:00

9.5 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
what runs on it proposed 2026-10-01 jochen false 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md

164. A setting is declared with its default, its meaning and what changing it costs

Context

The operator asked for one thing for every module, with the container runtime as the first case: one consistent default configuration for every machine, overridable per assignment, and easy to change later. The four machines' runtime configurations were each written by hand and differ — one keeps running containers through a daemon restart and one does not, their log rotation differs, and each names its resolver and its trusted registries in its own words.

Most of this was already decided. ADR 0046 said the definition is identity and defaults, the assignment's settings are the configuration, unset is the default, and an unknown setting is refused. ADR 0112 made a setting an operator requirement whose contract is "a type and, optionally, a default", answered by "the assignment's, or the requirement's default, or unresolved". An assignment is a module on a node, so the node layer of a module's settings already is the per-assignment override, and the mesh-wide layer is the one consistent default a person changes once.

What was built is narrower than what was decided, measured in the controller on the day of deciding:

  • Nothing declares which keys are settable. A mergeable file's content is its defaults, and every key of it — and every key not in it — is accepted. Nothing tells a person, or the console, what can be set, of what type, or what it means.
  • The refusal of an unknown setting is not there for most modules. The stray-setting report returns nothing at all for a module with any mergeable file, because such a file "takes any key" (issue 173 left files that way on purpose). It reports rather than refuses where it does run.
  • A value in a file that is not JSON can have no default. ${setting:<key>} (ADR 0155) is refused when no layer sets it — right for a mail domain, where a default is the very literal 0155 removes, and wrong for a tunable like the resolver's upstreams, which the resolver module therefore carries as literals in its file.
  • What a change costs is said per file, not per key. A service names the files it is reloaded or restarted on. The runtime re-reads its trusted registries on a reload and its dns key only when it starts; the resolver module declared a reload, so on two machines the key was written, reloaded, and never read, and every container got a public resolver for weeks while everything read as current (issue 110).

Considered Options

  1. Leave settings implicit; document each module's keys in its README. Rejected: a key the mesh does not know cannot be refused, typed, listed by the console or costed, and a README is a rule enforced by nothing.
  2. A second mechanism for tunables beside settings — defaults in a new block, settings untouched. Rejected: two ways to state one person's value, and design 27 already retires six mechanisms that grew that way.
  3. Settings declared in the definition, as 0112's operator requirement: a key, a type, a meaning, optionally a default, and what a change costs. Adopted.

Decision

A module declares every setting it takes. Each declared setting has a name, a type, one sentence of meaning, optionally a default, and what a change to it costs. The spelling is design 27's to settle with the rest of the requirement form; this record decides the content.

A setting with a default is a tunable; a setting without one is the operator's. A tunable resolves to its default when no layer sets it — wherever it is read, a mergeable file or ${setting:<key>} in a file of any format. A setting with no default is refused by name when nothing sets it, as 0155 decided; 0155's refusal is narrowed to exactly that case, not changed for it. Whether a value has a default is a fact about the software (a log size does, a mail domain does not), and the definition states it once.

The layers stay as they are, and every value says where it came from. The definition's default, then the mesh-wide layer, then the node's — later wins, objects merge, lists replace. One consistent configuration for every machine is the default plus the mesh-wide layer; one machine that differs says so in its own layer and nothing else. Asked for a module's configuration on a machine, the mesh lists every declared setting with its effective value and its source: default, mesh, or node.

Changing later is changing one of three places, and the plan shows its reach before anything moves. A new default ships with the module's next version and reaches every assignment that does not override it; a mesh-wide setting reaches every assignment of the module; a node's reaches one. The plan of a change names each assignment whose effective value moves.

A declared setting is the only kind accepted. Setting a key the module does not declare is refused when it is set, naming the declared keys, rather than reported when the machine is planned. A module that declares no settings keeps today's behaviour until it does; a catalogue test lists those modules, and the list shrinks to empty before the implicit form is removed — design 27's rule for every retired mechanism.

A setting says what it costs: nothing, a reload, or a restart. When a file changes, the host applies the strongest cost among the settings whose values moved in it, so a key the software reads only at start can no longer be written and never read. A service's reload-on and restart-on keep naming the files that are not settings — a generated roster, a credential.

The container runtime is the first module to declare its settings and the model for the rest: its log rotation, keeping containers through a daemon restart, and its resolver are tunables, and its trusted registries are what the mesh tells it.

Consequences

  • The console can show a module's settings as a form: what can be set, of what type, its default, and where the current value came from. That is the surface the operator wants for changing a default later.
  • settings set can refuse an unknown key, so ADR 0046's rule is enforced where it was only stated.
  • The resolver's upstreams, the runtime's log rotation, and other literals a definition carries because it could not give them a default become declared tunables.
  • What got harder: every module that takes settings must list them, and a mergeable file no longer silently accepts a key its author did not foresee. A person who needs one adds it to the definition, which is a new module version, not a setting.
  • Issue 173's open question — a consumer checks nothing against a contract — is unchanged; this record is the operator half of design 27's contract, not the provider half.
  • Not decided here: the spelling (design 27); whether a node's layer may be narrowed to a single key rather than replaced whole, as settings set does today.

How this is checked

Rule Checked by
Every setting a module takes is declared A parser test refusing a setting declaration without a type or meaning; a catalogue test listing modules with mergeable files or ${setting:} and no declarations, which must be empty before the implicit form is removed
A tunable resolves to its default; an operator value without one is refused Resolution tests: an unset tunable in a JSON file and in a text file both take the default; an unset setting with no default is refused naming it (0155's existing test)
An undeclared key is refused when set A controller test: settings set with an undeclared key fails naming the declared keys, and nothing is stored
Every effective value names its source A test listing a module's configuration on a node with one key from each of default, mesh and node
A change's reach is shown before it moves A plan test: changing a mesh-wide setting names every assignment whose effective value moves and no other
The strongest cost applies A host test: a file where a reload-cost key and a restart-cost key both moved restarts; a file where only reload-cost keys moved reloads
Live The container runtime's module lists its settings with their sources on every machine, and a mesh-wide change to its log rotation reaches all four at the next push

References