0081. The UI demo cluster is built by ichor init, pulling Ichor's images from a throwaway in-namespace registry
Status: Accepted · Date: 2026-10-26 · Design section: Bootstrap
Decided: The demo cluster's control plane is created by ichor init and its worker approved through ichor machine approve --install-disk, so every node's configuration is rendered by Ichor; images are pushed with crane to a registry:2 Deployment in ichor-e2e and reached through an anonymous spec.registries mirror of ghcr.io that ichor init --registry-mirror writes; init also gains --pod-subnets / --service-subnets, waits for the node, the API server and the webhooks, and applies the install manifest before the bundle Secret; the controller's shipped RBAC and cache are made consistent.
Context
The previous demo (up-demo) installed Talos with a config built by the test harness, so Ichor's render differed from the node's
config and its control plane could never converge (ADR 0077's drift). The user wanted the demo built with the product's own
bootstrap. Doing that on a workstation with no container runtime and no registry exposed product gaps.
Decision
- Harness.
up-demoonly creates the two nodes (name prefix fromICHOR_UI_DEMO_PREFIX, defaultichor-ui-demo) and leaves both in maintenance mode.ichor initruns from a busybox pod inichor-e2ebecause the workstation cannot reach pod IPs. - Images.
crane(added toflake.nix) appends the statically builtmanager/ichor-uibinaries as a layer ongcr.io/distroless/static:nonrootand pushes them through akubectl port-forwardto aregistry:2Deployment+Service (<prefix>-registry, emptyDir, inichor-e2eonly). The install manifest is built withmake build-ichor-embed IMG=ghcr.io/maeshinshin/ichor-controller:<tag> IMG_UI=ghcr.io/maeshinshin/ichor-ui:<tag>, so the image references stay realghcr.ionames and the nodes reach the registry only throughTalosCluster.spec.registries: hostghcr.io, endpointhttp://<registry ClusterIP>:5000. containerd tries the mirror first and falls back toghcr.iofor everything else (Talos's own images). No raw YAML and no patch: the typed field already existed. ichor init --registry-mirror HOST=URL(repeatable) fillsspec.registrieswith anonymous mirrors.initused to refuse any registry because a new cluster has no Secret to resolve credentials from; it now refuses only an entry carryingauthSecretRef, which the flag cannot produce.ichor init --pod-subnets/--service-subnetssetspec.network(ADR 0077); each is validated as at most two CIDRs. The demo uses 10.248.0.0/16 and 10.249.0.0/16 because the outer cluster owns 10.244.0.0/16.initordering and waiting, all found by running it against a real node. Bootstrapping etcd retries while the node answersUnavailable(still serving its maintenance certificate) orFailedPrecondition("bootstrap is not available yet"), up to 15 minutes.initwaits for the API server before the first write. It applies Ichor's manifest before the bundle Secret, because the Secret lives in the namespace the manifest creates (it failed with "namespace not found"). The CRs are created with a retry while the API server cannot call Ichor'sfailurePolicy: Failwebhooks, whose controllerinithas only just started. The bootstrapTalosConfigPatch(it carriesmachine.typeandcertSANs) moves from theclusterlayer to therolelayer targeting the first node's role: at the cluster layer it forced every later machine, a worker included, to render as a control plane. An existing cluster made by an olderinitkeeps the cluster-layer patch; recreate it withlayer: roleandtarget.role: controlplane.- Controller RBAC and cache. The shipped Role grants Secret
list/watchinichor-systemonly and the webhook configuration only byresourceNames, but the manager's cache listed Secrets cluster-wide and cert-controller's informer lists everyValidatingWebhookConfiguration; both were forbidden, the cert rotator's cache never synced and the pod crash-looped (the old demo hid this behindcluster-adminandENABLE_WEBHOOKS=false). The Secret informer is now confined toPOD_NAMESPACE, andlist/watchonvalidatingwebhookconfigurationsare granted withoutresourceNames(an informer cannot name its object);get/update/patchstay restricted to Ichor's one configuration. This widens the controller's read access to webhook configurations cluster-wide, read-only. The same bug hit real hardware forJob: the self-upgrade executor reads installer Jobs through the cached client, whose cluster-wide informer was forbidden (jobs.batch is forbidden ... at the cluster scope), so the installer Job was never created.controller.CacheOptions(moved out ofcmd/main.go) now scopesJobas well asSecrettoPOD_NAMESPACE, the Role grantsjobslist/watch, andcache_rbac_test.gobuilds the manager's cache as the controller's identity bound to the generatedrole.yamlunder envtest and requires an informer to sync for every kind read through the cached client. Pods, DaemonSets, ConfigMaps and Deployments are read through the uncachedAPIReaderor the direct client and need onlyget. - ichor-ui needs
POD_NAMESPACE.cmd/ichor-uiresolves the secrets bundle inPOD_NAMESPACE, but neither the chart norconfig/manager-uiset it (the old hand-built demo did, hiding it), so every machine page failed with "controller namespace is empty". Both now set it from the downward API, andTestPodNamespaceFromDownwardAPIpins it on the manager and ichor-ui containers.
Not done
- The registry is plain HTTP and unauthenticated, reachable by anything in the outer cluster; it is a demo fixture.
cranepushes the base image's layers too, so the registry holds a copy of the distroless base.- The demo uses the
nodeendpoint type with the node's pod IP, which changes if the node restarts; a restart needs a re-init.