0034. Config editing UI: the layer-grouped effective-config view, and why the rest waits on proto

Status: Accepted · Date: 2026-10-26 · Design section: CLI and UI, Patch layers

Decided: built the one piece of phase 6's "config editing by layer ... effective-config view annotated by origin and secrets masked" that the current API surface actually supports — grouping GetEffectiveConfig's values by origin.layer in the design's own bottom-up order — and did not build layer editing, impact preview, or schema-driven forms, because none of them have a backend to call yet.

Context

Roadmap phase 6 is "config editing UI." Architecture.md's ## CLI and UI section names four things: "config editing by layer," "an effective-config view annotated by origin and secrets masked," "impact preview before applying," and "schema-driven forms." Before building any of it, checked what proto/ichor/v1alpha1/ actually exposes today — unchanged since ADR 0016, still only ClusterService, MachineService and NodeService, none of them with a write method for TalosConfigPatch:

  • NodeService.GetEffectiveConfig is read-only and already returns exactly EffectiveConfigValue{path, value, secret, origin{layer, source}} per leaf (ADR 0018) — the "effective-config view annotated by origin and secrets masked" clause is already fully backed.
  • There is no PatchService (or any method on an existing service) to list, create, update or delete a TalosConfigPatch. "Config editing by layer" means writing one; there is nothing to call.
  • pkg/fieldmeta.Assess(paths []string, pinned *VersionContract) Impact is the whole implementation of "impact preview" — but it takes the set of changed paths as input. Nothing changed exists without an edit to diff against, and no RPC passes a proposed change anywhere, so this function cannot be invoked from the browser's side of any existing call. It is also Go-only, not reachable from the UI directly even if an edit existed to feed it.
  • No RPC returns a CRD's schema. TalosConfigPatch's full shape (the typed-config mirror types ADR 0003 generates) is baked into config/crd/bases/ichor.maesh.dev_talosconfigpatches.yaml's committed OpenAPI schema, but nothing serves that schema to a browser that only talks to the backend, not the Kubernetes API server (architecture.md, "Trust").

Decision: build the effective-config view's layer grouping, nothing that needs a write path

ui/src/pages/NodeDetail.tsx's EffectiveConfigSection already called GetEffectiveConfig and rendered one flat table (built in the phase 5 unit, before this one). It did not satisfy "the effective-config view in the UI shows which layer each value came from" (architecture.md, "Patch layers" — the one concrete UI requirement that section states): origin was a text column to read per row, not a grouping. groupByLayer buckets EffectiveConfigValue[] by origin.layer and renders one table per layer, in ConfigLayer's own numeric order — which is already the design's bottom-up order (system, cluster, role, selector, machine), so no second ordering table was needed. A short, explicit line above the tables says this view is read-only and that editing isn't available from this screen yet, rather than leaving a reader to wonder why there's no edit button.

Not built, and not stubbed: a patch-editing form, an impact-preview panel, or a schema-driven field list. Ponytail's first question — does this need to exist — has a clear answer here: a form with no RPC to submit to is not a smaller version of the feature, it is a non-functional one that looks finished. Building it would also have meant exactly the mistake the task warned against twice over: hand-listing TalosConfigPatch's fields (when mirrorgen exists specifically so no parallel catalogue of Talos's field names has to exist, ADR 0003) and hand-porting pkg/fieldmeta's reboot/install-time verdicts into JS (a second, harder to keep in sync, copy of a table whose own package doc explains at length why each entry needed real verification against Talos's source rather than being guessed).

Gap report: what proto needs for the rest of phase 6

Not added, per the task's own instruction — reported with enough shape that implementing them is mechanical, not another research pass:

  1. A write path for TalosConfigPatch. Smallest useful shape: a PatchService with ListPatches(cluster ObjectRef) → []Patch, GetPatch, CreatePatch, UpdatePatch, DeletePatch, mirroring MachineService's own shape (ADR 0016) rather than inventing a new one. Patch needs at least ref, clusterRef, layer, priority, target, documents[] — the same fields TalosConfigPatchSpec already has; the wire message should mirror the CRD spec, not reduce it.
  2. Impact preview needs a method that takes a proposed edit and returns pkg/fieldmeta.Assess's result. Either a dry-run field on CreatePatch/UpdatePatch (compute the diff server-side against the currently-applied config, call Assess, return the Impact alongside or instead of writing), or a standalone PreviewImpact(patch) → {rebootRequired bool, installTimeOnly []string, unsupported []string} — fieldmeta.Impact's three fields, unchanged. Either way, the UI needs to show installTimeOnly and rebootRequired as genuinely different outcomes, not two severities of the same warning: the design task that produced this ADR was explicit that a reboot does nothing for an install-time-only field, so presenting them identically would invite an operator to reboot for no reason.
  3. Schema-driven forms do not need a new RPC — config/crd/bases/ ichor.maesh.dev_talosconfigpatches.yaml's OpenAPI schema is already committed and already generated from the same mirror types a form would need to render (ADR 0003), and the browser already never talks to the Kubernetes API server directly, so serving that one YAML file (or a JSON-converted copy) as a static asset — no live cluster state in it — doesn't cross the "browser talks only to the backend" line either way. This is recorded here rather than built now because a form with nothing to submit to (point 1) is not worth building yet, not because the schema itself is hard to reach.

Consequences

  • ui/src/pages/NodeDetail.tsx's effective-config view groups by layer; ui/src/lib/, proto/, internal/uiapi/ are otherwise untouched by this unit.
  • No proto change. buf lint/buf breaking were not run against a diff because there is no proto diff.
  • The three gaps above are not new discoveries — ADR 0016 and ADR 0018 already named "config editing, a different, later unit" and "disk roles and network link configuration join this request message, additively, once they land on the CRD" as deferred. This ADR is the first unit that actually needed them and found no workaround, so it records the concrete shapes rather than leaving the gap as a one-line mention.
  • TalosOperation's progress view, the GitOps page, PR mode and OIDC remain out of this unit's scope, per the task's own boundary.

Search the docs

Search pages and headings