ADR 0040: Product-authored attributes of a machine live on one NodeIdentity per node, and never on a CR that copies the Node¶
Status: Accepted Date: 2026-08-06 Issue: #1243
Context¶
Production hostnames are rarely chosen for readability, so
#1210 gave a
chassis a dcs.io/display-name annotation and every surface renders it. The
annotation lives on the Node and
#1221 settled
that every surface reads it back from there at request time, so a name the
deployment layer set at enrolment and a name an engineer typed in the UI are
equally authoritative and there is one answer to what a machine is called.
#1229 gave
the Controller detail an editor for it: an engineer writes
Controller.spec.displayName and the physical operator applies it to the bound
node. A node that is not a Controller — a control-plane node, an
unclassified worker — has no product-owned object to author through, so it had
no editor at all and naming it meant kubectl annotate. The founder split that
remainder out of #1229 on 2026-08-04 rather than letting a whole design question
ride on one string.
The obvious fix is closed, and it stays closed¶
The gateway cannot write the annotation. ADR 0034
gives it no write privilege against a Node, and Kubernetes RBAC cannot scope
a patch to one annotation key. There is no narrow grant to write: permitting
the annotation permits spec.unschedulable, spec.taints and
metadata.labels with it, which is the plant-wide denial-of-service surface
ADR 0034 closed. Any proposal that begins "just let the gateway patch the node"
is proposing that grant, whatever it says about intent.
And so is the other obvious fix¶
ADR 0033 § 4 forbids a CR that mirrors
an object the cluster already reconciles, and forbids a Server CRD by name on
that ground: kubelet reconciles the Node continuously, so a CR carrying a copy
of it is stale by construction and hands an operator a second answer that
disagrees with the first.
What is left, and the question that has to be answered first¶
An object that records authored intent rather than node state. That is the
same basis ADR 0034 used to permit NodeMaintenance, and it survives ADR 0033
because nothing in it is a copy of anything the cluster writes.
#1243 asked the design question explicitly, and it is the reason this ADR exists rather than a commit message: does that object carry only the display name, or every product-authored node attribute? A rack position and an asset tag are the obvious next two, and a CRD per string is not a pattern worth starting.
Decision¶
Product-authored attributes of a machine live on a cluster-scoped
NodeIdentity, one per node, named for the node it authors. The physical
operator converges it onto the Node; the gateway writes only the
NodeIdentity; every surface keeps reading the resulting name back from the
node at request time.
1. One object per node, not one per attribute¶
NodeIdentity.spec carries displayName today. A second product-authored
attribute becomes a second field on the same object, not a second kind.
What may never go in it is anything the cluster reconciles. That is the line that keeps this object on the right side of ADR 0033 § 4, and it is the line a future field has to be checked against: the test is not "is it about a node", it is "does anything else author it".
2. It is named for the node, and the reconciler enforces that¶
metadata.name is the node name, and a spec.nodeName that disagrees is
Refused rather than applied. Node names are unique across a cluster
(ADR 0033 § 8), so this makes the apiserver the thing that prevents two objects
claiming one machine, rather than a list scan that would have to pick a winner
and would pick a different one on a different day.
3. A controller-bound node is named through its Controller, and this refuses¶
Controller.spec.displayName (#1229) keeps naming the nodes a Controller has
adopted. A NodeIdentity for such a node is Refused, with the owning
Controller named in the status so the operator knows where to type instead. The
gateway makes the same refusal synchronously.
Two authors converging on one annotation would each undo the other on every
reconcile, and ADR 0033 § 4 already says one physical thing gets one record.
The binding tested is Controller.status.nodeRef — the product's own record of
the adoption (ADR 0004) — because that is exactly the condition under which the
Controller reconciler writes, so the refusal and the write cannot disagree.
4. Clearing takes back only what this product applied¶
status.appliedDisplayName records the value the operator last wrote. An empty
spec.displayName removes the annotation only while the node still carries
that exact value. A name the deployment layer set survives being cleared here,
and a name somebody changed by hand between the apply and the clear is left
alone with tracking dropped.
This is reconcileNodeDisplayName's rule, deliberately reused rather than
re-derived. Two ownership models for one annotation would be worse than the two
authors this ADR just forbade.
5. Every disagreement is a sentence, never an empty field¶
The authored value and the node's value legitimately differ in three ways, and each is reported rather than left to be inferred from an editor that seems not to work:
| What is true | What the operator is told |
|---|---|
| The node was named at enrolment and nothing was authored here | Where the name came from, and that clearing here cannot remove it |
| An edit is authored and not yet applied | That the request is recorded and the operator is applying it |
| A name was cleared here but the node carries something else | That it was left in place, and what it now says |
The surface reports the request rather than the rename, because the gateway has not touched the machine and the annotation it can read is still the old one.
6. servers:set-display-name sits at PermAdmin¶
The default for a new action (ADR 0034 § 6, founder ruling 2026-08-04), and no exception is claimed. The act looks presentational, but this name is what alarms, audit records and every operator surface call the machine, so a wrong one misdirects an engineer to the wrong box. A plant that wants a narrower grant adds the action to a role's ADR 0024 allow list, which keeps the decision a reviewable line in its roles file.
Alternatives Considered¶
Let the gateway patch the Node annotation. One object fewer and no
convergence delay. Rejected: no such grant exists to write. Kubernetes RBAC has
no sub-resource granularity for a single annotation key, so this is the
spec.taints grant with a different justification.
A Server CRD carrying the name. Rejected by ADR 0033 § 4 by name, on the
drift argument #1168 made.
A ConfigMap in the operator's namespace. No new kind, no CRD to install. Rejected on four counts: no schema or validation, no RBAC granularity separating this from every other ConfigMap the operator reads, no status to report the Refused and Pending outcomes above, and no canonical address (ADR 0015). A CRD is this codebase's idiom for a record with a lifecycle, and this record has one.
A CRD per attribute (NodeDisplayName, later NodeLocation). Each object
would be trivially simple. Rejected explicitly in #1243: the second one arrives
soon, and a kind per string is a pattern that only gets worse. It would also
multiply the RBAC surface and the reconciler count for no gain.
Make NodeIdentity the single author and have Controller.spec.displayName
delegate to it. Genuinely cleaner, and the shape this may eventually take: one
author for one annotation, with the Controller detail editing a NodeIdentity
behind the scenes. Not taken now because the founder scoped #1243 to the
non-Controller case deliberately, and migrating a shipped field is a change with
its own upgrade story. The two authors are kept apart by the § 3 refusal rather
than by hoping they never overlap, so this stays a future simplification rather
than a latent bug.
Do nothing, and leave kubectl annotate as the answer. Rejected because the
machines an operator most wants to name are exactly the ones with no Controller:
the control-plane boxes named after their provisioning template, which
#1174 has
appearing in every System screenshot.
Consequences¶
- A new cluster-scoped kind,
physical.dcs.io/NodeIdentity, and a reconciler in the physical operator. The gateway gainsnodeidentitiesin its role and no new privilege against aNode. - Renaming is asynchronous. The surface reports a request in flight, which is the same shape every other infrastructure action already has under ADR 0034.
- The second product-authored node attribute is a field on an existing object and a new column in an existing status, rather than a design conversation.
- A deployment that names its nodes at enrolment is unaffected and keeps authority over those names.