0023. TalosStorage and the longhorn-v1 disk preset

Status: Accepted · Date: 2026-10-25 · Design section: 4.4 (Storage)

Decided: Keep TalosStorage to layer 1 only -- a longhorn-v1 preset emitting the Talos-side config a node needs, never the storage system itself

Context

The design's ### Storage section splits storage work into four layers and gives Ichor the first (Talos-side preparation: volumes, extensions, kernel modules, sysctls) and the fourth (node-operation coordination: evacuate before drain). Layers 2 and 3 — installing the storage system and operating its volumes/replicas/settings — belong to Argo CD and the storage system's own controller, never Ichor. This ADR covers only layer 1: a TalosStorage CRD and a longhorn-v1 preset producing the Talos-side config a node needs before Longhorn can use one of its disks. Layer 4 (the node-operation hook: mark unschedulable, request evacuation, wait, drain) is a separate work unit and is not built here — TalosStorage.spec.evacuationTimeout exists so that hook has somewhere to read its timeout from, but nothing in this change calls it.

The design leaves one thing unresolved going in: the exact Longhorn node label/annotation and evacuation field names per version, listed as an open question. Resolving it (with citations, not assumption) was this task's other half.

What was verified, and against what

Checked against Longhorn's official docs (longhorn.io/docs/1.12.1 and 1.13.0, the latter current stable at the time of writing) and longhorn-manager's own source (github.com/longhorn/longhorn-manager, tag v1.12.1, k8s/pkg/apis/longhorn/v1beta2/node.go) — not recalled from memory, not guessed:

  • Node registration is two plain Kubernetes Node fields, not a Longhorn CR. The label node.longhorn.io/create-default-disk set to "config" tells Longhorn's node controller to read the disk list from the annotation node.longhorn.io/default-disks-config (a JSON array of {path, allowScheduling, [tags]}) instead of auto-creating a default disk from Longhorn's own default-data-path setting. This matches the design's "registered ... through its own node label and annotation mechanism, not by writing its CRs" directly, and has been stable across the two doc versions checked.
  • The matching "is this actually honoured" setting is Longhorn's own create-default-disk- labeled-nodes setting. If it is off, Longhorn ignores the label/annotation pair entirely and auto-creates (or doesn't) by its own default-path logic — this is the live/consistency check the design calls for ("a consistency check confirms the storage system is configured to honour that"), and is why Preset.CheckConsistency takes a LiveConfig.CreateDefaultDiskOnLabeledNodes bool rather than assuming the label is enough on its own.
  • Talos-side mount path changed between Talos versions, not Longhorn versions. Longhorn's Talos Linux support doc gives /var/mnt/longhorn for Talos ≥1.10 (a UserVolumeConfig-based setup) and /var/lib/longhorn for older Talos. Ichor targets Talos 1.14.3 (ADR 0003), so only the newer path applies; longhornMountPath in pkg/storage/longhornv1.go is the one value, not a version switch.
  • The required system extensions are siderolabs/iscsi-tools and siderolabs/util-linux-tools, per the same support doc. These are Image Factory schematic/installer-image concerns, not a TalosConfigPatch document (machine.install.extensions lists already-baked extension image refs — a different mechanism, verified during the v1alpha1.Config walk in ADR 0003 — it does not make an extension appear in an image that was not built with it). longhornV1.CheckExtensions therefore verifies and blocks with a clear error; it does not and cannot generate the extension into existence.

Not resolved, left to a later look if it matters: the exact nodes.longhorn.io CRD field names for eviction (NodeSpec.AllowScheduling/EvictionRequested, DiskSpec.AllowScheduling/EvictionRequested, per the same source read) are layer 4's concern, not layer 1's — recorded here since they were read during the same research pass, not because this change uses them. The open question in docs/design/architecture.md is removed because layer 1's half of it (registration) is now resolved and cited; layer 4's half (eviction) is this ADR's explicit non-scope, not a remaining unknown to track under the same bullet.

Decision

  • TalosStorage (api/v1alpha1/talosstorage_types.go, scaffolded with kubebuilder create api) stays exactly as thin as the design asks: spec.clusterRef (which TalosCluster), spec.nodes (a metav1.LabelSelector over Kubernetes Node labels — which nodes get storage prepared), spec.diskPreset (an enum, longhorn-v1 the only value for now), spec.diskSelector (a Talos disk-selector CEL expression, the same language provisioning.diskSelector.match already uses in VolumeConfig/UserVolumeConfig/ RawVolumeConfig), and spec.evacuationTimeout (*metav1.Duration, read by layer 4, not this change). status.conditions carries a kstatus-compatible Ready condition so, per the design, "Argo CD's sync waves can hold the storage Application until the Talos side is ready" — no reconciler sets it yet; that is controller work, not scaffolding work, and is not part of this change either.
  • pkg/storage.Preset is the same shape pkg/cni.Preset already established for the CNI hand-over: pure Go, no cluster calls, returns data for a caller to apply. RequiredExtensions/CheckExtensions cover the installer-image gate; TalosPatches returns the UserVolumeConfig document (named longhorn, matching Longhorn's own example) plus the Config document adding the kubelet bind mount at longhornMountPath the support doc's example shows alongside it — without the mount, the volume is invisible to the kubelet and so to Longhorn; NodeLabels/NodeAnnotations return the two Longhorn fields verified above; CheckConsistency implements the live-settings check. NewPreset is the only switch on diskPreset's string value, so a second preset (longhorn-v2, raw, filesystem, all named in the design's roadmap) is a new Preset implementation and one new case, nothing else.
  • Scaffolded, not hand-written. kubebuilder create api --group ichor --version v1alpha1 --kind TalosStorage --resource --controller=false generated talosstorage_types.go's skeleton, PROJECT, and the three kustomization.yaml entries; --controller=false because layer 1's reconciler is not part of this change (there is nothing yet for one to do: Preset is a library, not wired to a controller). make manifests generated the CRD, RBAC roles and deepcopy; dist/ chart's CRD and RBAC templates were synced by hand (the header-reconstruction technique already established for TalosConfigPatch/TalosCluster), never via kubebuilder edit --plugins=helm/v2-alpha, which would revert unrelated hand-edits already in the chart.

Consequences

  • Layer 1 is complete and tested (pkg/storage, 95%+ coverage): given a disk preset name and a disk selector, Ichor can produce the exact TalosConfigPatch documents and node labels/ annotations Longhorn needs, and can tell the caller before anything is applied whether the node's installer image is missing a required extension.
  • Nothing in this change installs, configures, or talks to Longhorn, writes a nodes.longhorn.io resource, or reconciles TalosStorage. Those are, respectively, Argo CD's job, out of scope by design, layer 4 (a separate work unit), and controller work for a later change once there is a reconciler to write.
  • A second preset is additive: implement Preset, add a case to NewPreset, extend the CRD enum. Nothing about TalosStorageSpec assumes Longhorn specifically.

Alternatives considered

Writing a Longhorn nodes.longhorn.io resource directly instead of using the label/annotation mechanism. Rejected: the design is explicit that disks are registered "through its own node label and annotation mechanism, not by writing its CRs," and Longhorn's node controller already owns reconciling that CRD from the label/annotation pair — writing it directly would race the exact controller meant to own it.

Giving TalosStorage a --controller=true scaffold now, even with an empty Reconcile. Rejected as the kind of placeholder ponytail mode rules out: an empty reconciler is not load-bearing, adds a watch and an RBAC surface for nothing, and the next change that actually needs one can scaffold it then.

Amendment (ADR 0050)

Layer 1 is now rendered into node configs as the system layer and TalosStorage has a status controller (ADR 0050). Wiring it to Talos 1.14.3's validator changed the preset: TalosPatches returns only the UserVolumeConfig (now with maxSize: 100%, which Talos requires), the kubelet mount moved to KubeletMounts(), and node registration is rendered per version contract.

Amendment (2026-10-27): a schematic-only change is a Talos upgrade

Adding an extension (iscsi-tools, util-linux-tools) means a new schematic, and "this is a Talos upgrade to a new schematic" has to be true when spec.talosVersion does not move. On hardware it was not: talos_upgrade.go compared only the running version with the target, so changing only TalosCluster.spec.schematic.id completed the TalosOperation in seconds with no reboot.

A machine now needs the upgrade when its running version or its running schematic differs from the target; the same version and schematic is still a no-op. The Upgrade call and the post-reboot check (a node that comes back on the old schematic fell back to its previous A/B slot) use the same rule.

How the running schematic is read: the node's ImageFactorySchematic resource (runtime.ImageFactorySchematics.runtime.talos.dev, singleton image-factory-schematic, read over COSI like Nodename). Talos fills it from the schematic extension that Image Factory bakes into every asset and installer image, so it names the schematic of the system actually booted, and changes only through an upgrade. Rejected: the extensions.talos.dev/* Node labels (they list extension names and versions, not the schematic ID, and are written by a Kubernetes-side controller that lags a reboot); the installer image in TalosMachine.spec.install.image (what was asked for, not what runs); the extension list (two schematics can carry the same extensions). A node that booted no Image Factory asset has no such resource and is taken to run the stock schematic (pkg/upgrade.DefaultSchematic), which is what spec.schematic unset also means.

Status fields: TalosMachine.status.schematic records what the machine controller read on its last successful reach of the node, so the plan (which cannot dial nodes) can say "schematic X → Y". PlanOperationResponse gains current_schematic and target_schematic, set only for a Talos upgrade in which some approved machine's schematic differs; the CLI and UI headlines use them.

Amendment: the extension check is wired

CheckExtensions is now called by the TalosStorage status controller, which sets the informational ExtensionsPresent condition from the Nodes' extensions.talos.dev/* labels (ADR 0050 amendment).

Search the docs

Search pages and headings