0030. ichor ui: find the pod through its Service, reforward on failure
Status: Accepted · Date: 2026-10-26 · Design section: CLI and UI
Decided: ichor ui discovers ichor-ui pods through the ichor-ui-service Service's own selector/port and reforwards to another ready pod (same local port) whenever the tunnel ends, instead of targeting one pod name
Context
architecture.md's "CLI and UI" section is explicit: "In the first release the
UI is reached through ichor ui (a port-forward) ... ichor ui
re-establishes its port-forward to another pod when the current one goes
away." cmd/ichor-ui (the server) and internal/uiapi (its handlers) already
exist; nothing about reaching it from the CLI did until this unit. The chart
(dist/chart/values.yaml's ichorUI.replicas: 2) assumes more than one pod
exists specifically so a drain or a rollout never takes the UI down -- ichor ui has to actually survive that, not just document that it should.
Decision: discover pods through the Service, not a fixed label guess
runUI (internal/cli/ui_run.go) first Gets the fixed-name
ichor-ui-service Service (dist/chart/templates/ui/ui-service.yaml's own
comment: "Fixed name, not release-templated ... a future ichor ui
port-forward ... need[s] a predictable name") and uses its .spec.selector
and .spec.ports[0].port to list pods and pick a target port. The
Deployment's pod-template labels are release-templated
(include "ichor.name" .) and so not safe to hardcode; the Service's own
selector is already whatever that template resolved to, so reading it back
avoids guessing a label value this command has no other way to know.
Decision: a reforward loop, not a single kubectl port-forward call
selectPod (pure, internal/cli/ui_run.go) is the actual decision: among
ready pods (Running, PodReady condition True, no
deletionTimestamp), prefer one other than the pod that just failed,
falling back to it if it is the only ready pod there is (the
replicas: 1 case). runUI wraps this in a loop: list pods, forward to
whichever selectPod returns, and when that Forward call ends for any
reason (an error, or a clean return with the context not yet cancelled --
both look the same from a lost SPDY stream, e.g. the pod being deleted
mid-drain), print what happened and go around again, excluding that pod's
name. The local port never changes across a reforward, so a browser tab
already pointed at localhost:<port> keeps working without the user doing
anything.
Each iteration prints to stderr before and after: "forwarding
localhost:N -> pod/X" (first attempt) or "re-established the port-forward
-> pod/X" (every attempt after), and "lost the port-forward to pod/X
(err); looking for another pod..." when it ends. This is the design's
"does not break" requirement applied to a CLI rather than a controller:
nothing here blocks silently, and a user watching the terminal can always
tell whether ichor ui is still working or has found a problem.
Decision: the real tunnel is client-go's own tools/portforward, swapped out in tests
ui_portforward.go's portForwardToPod is the one place that touches a
real cluster: transport/spdy.RoundTripperFor plus
tools/portforward.NewOnAddresses, the same two client-go packages
kubectl port-forward itself is built on (not reimplemented --
ponytail's "already a dependency, use it" rung). uiDeps.Forward is the
seam: runUI's own tests (internal/cli/ui_run_test.go) pass a fake that
never opens a socket, only drives the ready/error/context-done paths
selectPod's decision depends on -- proving the reforward behavior and
the user-visible messages without a real cluster, the same
dependency-injection shape initDeps/adoptDeps already use for Talos
and Kubernetes calls elsewhere in this package.
Decision: open the browser with the dependency already in the module graph
k8s.io/kubectl already pulls in github.com/pkg/browser indirectly;
ui_browser.go's openBrowser calls browser.OpenURL directly rather than
hand-rolling the usual xdg-open/open/rundll32 OS switch, promoting it
from an indirect to a direct require in go.mod (also pulling
github.com/gorilla/websocket and github.com/moby/spdystream into
go.sum, both already-transitive dependencies of k8s.io/client-go's
tools/portforward/transport/spdy packages this unit imports directly for
the first time).
Consequences
ichor uiauthenticates as whoever runs it, through--kubeconfig, the same as every otherichorsubcommand -- no ServiceAccount or RBAC is added by this unit;pods/portforward,podsget/list andservicesget on the ichor-ui namespace are the cluster operator's own permissions to grant, same askubectl port-forwardneeds.- Not built here: anything in
cmd/ichor-uiorinternal/uiapi(read only, per this unit's own instructions) --ichor uionly ever reaches as far as the TCP tunnel; what runs over it is already built.