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.GetEffectiveConfigis read-only and already returns exactlyEffectiveConfigValue{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 aTalosConfigPatch. "Config editing by layer" means writing one; there is nothing to call. pkg/fieldmeta.Assess(paths []string, pinned *VersionContract) Impactis 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 intoconfig/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:
- A write path for
TalosConfigPatch. Smallest useful shape: aPatchServicewithListPatches(cluster ObjectRef) → []Patch,GetPatch,CreatePatch,UpdatePatch,DeletePatch, mirroringMachineService's own shape (ADR 0016) rather than inventing a new one.Patchneeds at leastref,clusterRef,layer,priority,target,documents[]— the same fieldsTalosConfigPatchSpecalready has; the wire message should mirror the CRD spec, not reduce it. - Impact preview needs a method that takes a proposed edit and
returns
pkg/fieldmeta.Assess's result. Either a dry-run field onCreatePatch/UpdatePatch(compute the diff server-side against the currently-applied config, callAssess, return theImpactalongside or instead of writing), or a standalonePreviewImpact(patch) → {rebootRequired bool, installTimeOnly []string, unsupported []string}—fieldmeta.Impact's three fields, unchanged. Either way, the UI needs to showinstallTimeOnlyandrebootRequiredas 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. - 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 breakingwere 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.