0050. TalosStorage layer 1 renders as the system layer, selected by one shared rule, with a status controller

Status: Accepted · Date: 2026-10-26 · Design section: Storage, Patch layers Decided: Render a TalosStorage's Talos-side preparation as part of the system layer (below every patch), choose the governing TalosStorage with one storage.Select shared with the evacuation hook, hand Render a single *SystemInputs instead of a growing argument list, and report Ready from the convergence of the machines it governs

Context

ADR 0023 built the longhorn-v1 preset but nothing rendered it; ADR 0025/0026 wired layer 4 (evacuation). Layer 1 never reached a node and TalosStorage.status was never written, so Argo CD's sync waves had nothing to wait on. Wiring it to the real Talos 1.14.3 validator also showed that the preset ADR 0023 shipped could not have worked (below).

Decisions

1. Patch layer: system. The preparation is merged after the base config and the registries (ADR 0027) and before every TalosConfigPatch. It is Ichor-generated and not patch-shaped; a user who needs a different mount option or disk size overrides it with a patch, and one who wants no preparation removes the TalosStorage (a patch cannot delete the volume). Verified: a machine-layer UserVolumeConfig named longhorn wins over it. A patch replacing that document must be complete (Talos then validates it alone: a diskSelector is required).

2. Which nodes. storage.Select(cluster, storages, nodeLabels) is the only implementation of the rule: same namespace and clusterRef, spec.nodes matches, several matches resolve to the first by name. LonghornEvacuator.Resolve (layer 4) and render.StorageFor (layer 1, also used by the input fingerprint and the status controller) both call it. The label source differs on purpose: evacuation matches the live Kubernetes Node (it needs the Node to exist); rendering matches TalosMachine.spec.nodeLabels, the declared labels, because at first render no Node exists and a render that depended on live Node state would flap. A selector should therefore use labels set through spec.nodeLabels. (When this ADR was written spec.nodeLabels was not rendered onto the Node, so layer 1 and layer 4 could disagree; ADR 0052 renders it.)

3. Render's input. The auth parameter became sys *SystemInputs (registry credentials plus the namespace's TalosStorages), built by render.ResolveSystemInputs. It is still one required positional argument, so a caller that forgets to resolve it does not compile; the next system-layer input extends the struct, not the signature. nil means none and is for tests and ichor init. The machine controller resolves it once per reconcile and feeds the same value to Render and to desiredInputsHash, which now includes the governing TalosStorage's generation, so editing one reads as a desired-state change and not as drift. The machine controller also watches TalosStorage so converged nodes pick it up.

4. Status. Ready (what Argo CD reads) is True when at least one approved machine is governed by this TalosStorage and every such machine is Ready=True with an observedInputsHash equal to the one it would compute now (a machine that converged before the TalosStorage existed does not count). False reasons: NoNodesSelected, NodesNotConverged (lists the machines), ClusterNotFound. StorageSystemConsistent is informational and never gates Ready: it reads Longhorn's settings.longhorn.io object create-default-disk-labeled-nodes (top-level value, longhorn-manager v1.9.1 setting.go / types/setting.go, name verified there) through the preset's CheckConsistency. Longhorn or its CRD absent is Unknown/NotInstalled, not an error. It must not gate Ready: the setting lives in the storage Application's values, which the held wave delivers. The read goes through mgr.GetAPIReader() (no informer on a possibly absent CRD) and is repolled every two minutes. Ichor writes no Longhorn object here. Not done: the extension check (CheckExtensions). Nothing in Ichor knows a node's installed extensions (the cluster only holds an opaque schematic ID); it needs a Talos API read and is left for that work. Argo CD's Lua health check for TalosStorage ships with this (ADR 0046).

5. No storage system is installed or operated. Disks register through the Node label and annotation the preset returns; Longhorn objects are only read (setting) and, by layer 4, written as ADR 0025 already decided.

What wiring it to Talos 1.14.3 corrected in the ADR 0023 preset

  • UserVolumeConfig is rejected without minSize or maxSize: the preset now sets maxSize: 100%.
  • Under a v1.14 contract the base config uses standalone KubeletConfig and KubeNodeConfig documents, and Talos rejects the legacy machine.kubelet / machine.nodeLabels next to them. The preset no longer returns the legacy Config document: TalosPatches is the volume only, KubeletMounts() is separate data. Render puts the Node label/annotation into a KubeNodeConfig document under 1.14+ and into machine.nodeLabels, nodeAnnotations and kubelet.extraMounts below it (tested with v1.12).
  • The standalone KubeletConfig has no extra-mount field, so under 1.14+ the kubelet bind mount from Longhorn's Talos doc is not rendered. Talos itself bind-mounts /var/mnt into the kubelet (rslave, read-only); whether Longhorn works on that without the rshared read-write mount is not verified here and needs a real node.

Also fixed

The controller's role had no rule for talosstorages, longhorn.io nodes/replicas, so layer 4 could not have run in-cluster. Rules added (cluster-scoped, since the Longhorn namespace is not fixed and a RoleBinding into a namespace that may not exist would fail the install): talosstorages get/list/watch, talosstorages/status, Longhorn nodes get/update, replicas list, settings get restricted to the one setting name.

Consequences

  • Kernel modules and sysctls: longhorn-v1 emits none today (ADR 0023 verified none); a preset that needs them returns them from TalosPatches and they arrive the same way.
  • Patches do not trigger machine reconciles (only TalosStorage does); unchanged here.

Amendment: the UI backend reads TalosStorage

The UI's effective-config view renders layer 1, so the ichor-ui ClusterRole needs list on talosstorages (missed at first, found live; see the amendment in ADR 0018, which also adds the test that enforces it).

Amendment: the extension check is wired (ExtensionsPresent)

Found on real hardware: a longhorn-v1 TalosStorage was Ready on nodes whose schematic lacked iscsi-tools and util-linux-tools, and longhorn-manager crash-looped. The "Not done" note above is superseded. TalosStorageReconciler now sets ExtensionsPresent (informational, never gates Ready, never blocks the rest of the reconcile): for each governed machine it finds the Kubernetes Node (spec.hostname, else the Node whose address is the machine endpoint) and reads its extensions.talos.dev/<name> labels, mapping each to siderolabs/<name> for Preset.CheckExtensions. False/MissingExtensions names each node and its missing extensions and says to add them to spec.schematic and run ichor upgrade talos; Unknown/NodesNotObserved when no governed machine has a Node yet; True/AllPresent otherwise.

Why the Node labels and not the Talos API: Talos itself publishes them from the booted image, the controller already reads Nodes (no new credentials, no Talos client or per-node dial in a status controller), and it reflects what is running, not what a schematic ID claims. The cost is that a node not yet joined has no labels, hence Unknown rather than False. The UI shows the condition in the generic condition list, and ExtensionsPresent=False renders as a failure.

Search the docs

Search pages and headings