0104. A cluster with no CNI gets Ichor's Cilium before Ichor, and Ichor is installable with Helm
Status: Accepted; built; hardware-verified, E5 and E6 included (E5 passed on 2026-10-30 after the pending-record repair; see Hardware results) · Date: 2026-10-29 · Design section: Bootstrap (ichor adopt), Cilium · Builds on: 0098, 0099, 0101, 0103 · Amends: 0103 (the "no CNI" row of 1.1, 1.1a's gate wiring, test D5, open question 3)
Decided: ichor adopt on a Talos cluster with no CNI installs Ichor's pinned Cilium itself, in the adopt process with the Helm SDK as Ichor's own release record in ichor-system, before Ichor's own pods are installed, and adopts the cluster as the managed cilium preset (so ADR 0103's "no CNI becomes unmanaged talosCNI: none" row is replaced, and --cni-unmanaged is refused there); a cluster that already has a CNI keeps ADR 0103's rules and its network-policy gate follows live detection; every wait for Ichor in adopt and init is bounded and names why the pods are Pending; and Ichor can be installed with helm install of dist/chart (published as an OCI chart), after which ichor adopt detects the existing install, skips its own install step and registers the cluster, with a new ichor cni bootstrap for the one case Helm cannot cover, a cluster with no pod network.
In this ADR, adopt means: bring an existing, running Talos cluster under Ichor's management without rebuilding it.
Context
Hardware testing of ADR 0103 found two defects.
- D5, no-CNI cluster hangs. ADR 0103 adopts a cluster with
cluster.network.cni.name: noneand no CNI aspreset: unmanaged, talosCNI: none. But that mode installs Ichor into the cluster (open question 4 was answered "adopt installs Ichor"), and without a CNI the nodes areNotReady, so Ichor's controller, webhook and UI pods stay Pending. The adopt write phase then waits for the admission webhooks (createWhenWebhooksUp, 30 minutes by default, retrying silently) and gets nowhere. The unmanaged mode was never able to work for this input; ADR 0103 left "installing Cilium onto it" as open question 3. - D3, policies skipped on a hands-off Cilium cluster. ADR 0103 1.1a says Ichor's
CiliumNetworkPolicyobjects follow live detection (installer.IncludeCiliumPolicies(detected, crdPresent)).adopt_run.gocomputes that gate correctly, butadoptWriter.installIchorthen callsapplyNetworkPolicies(..., preset, ...)with the cluster's CNI preset, and that function returns early when the preset is notcilium(init_netpol.go, "does not enforce CiliumNetworkPolicies"). Forpreset: unmanagedthe policies are never applied, whatever the gate said.
The user decided (2026-10-29, in Japanese): for a cluster with no CNI, install Cilium; for a cluster that has a CNI, install with CNI management off (the existing unmanaged mode); and make Ichor installable with Helm too.
Constraints that stay: Cilium is Ichor's only CNI and Ichor's own Helm release (CLAUDE.md, ADR 0099/0101); the controller is the only writer to nodes and never mutates spec; adopt itself never writes to a node (ADR 0014, 0103); Ichor does not install storage, monitoring or GitOps tooling (ADR 0101); machine config is typed only; clean-room (nothing from Sidero Omni was read for this ADR).
Facts this ADR relies on (read from the tree at 9873f59)
cilium.Apply(ctx, cfg, vals, opts)(pkg/cni/cilium/helm.go) is the one place that installs or upgrades the releaseciliuminkube-system; with no Ichor record it runsaction.InstallwithTakeOwnership, otherwiseUpgrade. Theichor cni helm applyJob (internal/cli/cni_helm.go,runCNIHelmApply) is a thin caller: it builds a Helmaction.Configurationwhose storage namespace isichor-system(newHelmConfig) and callsApply. The Job pod hashostNetwork, tolerates everything and reaches the API throughlocalhost:7445(KubePrism) (internal/controller/cni_helm.go).- Effective values come from
cilium.EffectiveValuesForMode(cluster, mode);k8sServiceHost/k8sServicePort(localhost:7445),kubeProxyReplacementandtunnelProtocolare Ichor-owned (ADR 0099 decision 3). - The hand-over reconciler's signal is "Helm release
ciliumisdeployedat the effective-values hash and the DaemonSet is Ready" (ADR 0099 decision 2), not "the Job ran". It setsCNIHandover=True, and render drops the inlinecnientry (or the recordedtakenOverInlineManifest) only then. - Talos enables KubePrism on 7445 by default (ADR 0099 relies on it); a no-CNI Talos cluster still has a working control plane, because the control-plane static pods, kubelet, kube-proxy (if enabled) and Cilium itself use the host network.
dist/chartis a hand-maintained chart (Chart.yamlversion: 0.1.0,appVersion: "v0.1.0",kubeVersion >=1.30.0-0) with the CRDs undertemplates/crd(kept on uninstall:crd.keep,helm.sh/resource-policy: keep), the controller, webhook, UI, RBAC,ichor-cniRBAC (cniHelm.enabled: true), the optional installer identity (installer.enabled: false, "a chart-installed Ichor is upgraded through the chart") and the Cilium policies behindnetworkPolicy.cilium.enabled(null/true/false). It is not published anywhere today: no workflow pushes it, the only chart workflow ishelm lint/a kind test (.github/workflows/test-chart.yml), andvalues.yamlimage repositories are the placeholderscontrollerandichor-ui. The chart does not create theIchorInstallationobject;ichor initand the adopt install step do (createIchorInstallation).- Adopt's "Ichor is running" test is
ichorRunning: whether theIchorInstallationsingleton exists. A Helm-installed Ichor has none, so todayadoptwould apply the install manifest on top of the chart's objects.
Decision
1. A cluster with no CNI: adopt installs Cilium first, then Ichor
1.1 What "no CNI" means. All of these hold, read by the inspection (unit 2 of ADR 0103) with nothing written:
- Talos says so:
cluster.network.cni.name: none(or the CNI selection resolves to none). - Kubernetes finds no CNI workload: no Cilium, Calico or other CNI DaemonSet in any namespace, no
CiliumNodeCRD, no CNI config on the nodes that Ichor can read (the existing mixed/other detection of 1.1). - Every node is
NotReadywith a Ready condition whose message says the network plugin is not ready (cni plugin not initialized/ reasonNetworkPluginNotReady).
Partly true is not "no CNI": a cluster where some nodes are Ready, or where cni.name: none and a CNI was applied by the user by hand, is the existing CNI case (decision 2) and, for a CNI Ichor cannot classify, is refused as ADR 0103 already says. talosCNI: none stays a valid value of spec.cni.unmanaged, because it describes exactly that second kind of cluster (a user-applied Calico or Cilium with cni.name: none); it is no longer what a CNI-less cluster becomes.
1.2 The mode. The cluster is adopted with spec.cni.preset: cilium (managed). --cni-unmanaged on a no-CNI cluster is refused, with nothing written: "this cluster has no CNI, so no pod can run on it, including Ichor's. Run again without --cni-unmanaged and Ichor installs Cilium, or install a CNI yourself first." The table row of ADR 0103 1.1 becomes:
| Found | Result |
|---|---|
| No CNI installed (1.1 above) | Managed Cilium. Ichor installs its pinned Cilium (this ADR), then adopts. --cni-unmanaged is refused. |
1.3 Preconditions (checked read-only, before the plan asks for consent; each failure refuses with nothing written):
- Talos v1.12 or later on every node and the other adopt gates, unchanged.
- KubePrism is enabled on port 7445 on every node (
machine.features.kubePrismorKubePrismConfig). It is Talos's default, so this is normally true. If it is not, adopt refuses and says to enable it first: with no pod network Ichor has nothing it can safely re-order, and Cilium reaches the API throughlocalhost:7445(ADR 0099). This is stricter than ADR 0103 1.6 (which lets the first render turn KubePrism on) on purpose: KubePrism has to exist before Cilium can start, and the render is held until after Cilium. - Every node has a
spec.podCIDR(kube-controller-manager allocates node CIDRs, the Talos default), because Cilium runs withipam.mode=kubernetes. - The pod and service subnets are read from the Talos config (class A, already mapped to
spec.network).
1.4 What Cilium is installed with. Everything is chosen, nothing is detected from an existing Cilium:
spec.cni.tunnel: the API default (ADR 0098/0099);--tunnel vxlan|geneveon adopt overrides it before consent (immutable afterwards, as forinit).spec.cni.kubeProxy: from the Talos side alone. Talos kube-proxy enabled (thekube-proxyDaemonSet exists; hostNetwork, so it runs without a CNI) givesenabledandkubeProxyReplacement=false; Talos kube-proxy disabled givesreplacedandkubeProxyReplacement=true(KubePrism is already required by 1.3). There is no Cilium side to disagree with, so the refusals of ADR 0103 1.5 do not apply.- No
spec.cni.cilium.valuesunless--cilium-values FILEis given later by another ADR; adopt uses Ichor's defaults. TheTalosClusterobject is built in memory first (preset, tunnel, kubeProxy, annotationichor.maesh.dev/adopted), and the Cilium values areEffectiveValuesForMode(thatObject, mode); the write phase writes that same object. A test asserts the values installed in step 2 below equal the values the controller derives from the written object, so the controller sees a release at the right hash.
1.5 The bootstrap order. The adopt CLI installs Cilium itself, in process, before Ichor. The no-CNI path of the adopt phases (ADR 0103 1.8) is:
- Plan (read-only; prints "no CNI found: Ichor will install Cilium
(tunnel X, kube-proxy Y) as its own Helm release, then install Ichor, then adopt"). Consent is required, as for a takeover. - Cilium. The CLI creates the
ichor-systemnamespace if it is absent (Helm's record storage needs it; the install manifest later creates it as anAlreadyExistsno-op), builds the sameaction.ConfigurationasrunCNIHelmApply(records inichor-system, the operator's kubeconfig), and callscilium.Applywith the values of 1.4. It then waits, bounded (decision 3: 10 minutes), for the Cilium DaemonSet and operator to be Ready and for every node to report Ready. Nothing in this step needs a pod network: Cilium's agent and operator arehostNetworkand tolerate the not-ready taint, and they reach the API server through KubePrism. - Ichor. The install manifest, the
IchorInstallation, and (now that Cilium is Ready and its CRD exists) the Cilium policies, with the gate of decision 2. The controller and webhook pods schedule because the nodes are Ready. - Write CRs: bundle Secret,
TalosCluster, heldTalosMachines and patches, as in ADR 0103 1.8 phase 2. The webhook wait now succeeds. - Hand-over. The controller sees the release
deployedat the effective-values hash and the DaemonSet Ready and setsCNIHandover=True. There is no inline entry (the machine config never had one), so nothing is removed, and noichor-cniJob is created because the hash already matches (the unit that builds this asserts it; the Job is only a way to reachApply, not part of the signal). Adopt waits for it withwaitHandover, bounded. - Release the holds machine by machine, exactly as ADR 0103 phase 4.
1.6 The render matches; no class C. The live machine config of a no-CNI cluster is cluster.network.cni.name: none, no inline Cilium entry, KubePrism on, kube-proxy as recorded. The cilium preset renders cni.name: none, a $patch: delete of KubeFlannelCNIConfig (which deletes nothing that is there), KubeProxyConfig enabled:false only for replaced, KubePrism, and the inline cni entry only until CNIHandover=True. Because step 5 sets CNIHandover=True before step 6, the first render has no inline entry and equals the live config for every CNI-relevant path. A render test with a cni.name: none fixture asserts byte-equality of those paths (unit 3); hardware case E1 verifies it. If a node does differ, the existing class A/B/C machinery reports it and the hold keeps it unapplied.
1.7 The hold. Unchanged and now load-bearing in two places. The TalosMachines are created held (step 4), so the controller computes drift and applies nothing until step 6. The Cilium install in step 2 happens before any TalosMachine exists, so the controller cannot race it with a config apply. A rerun after a failure is safe: cilium.Apply goes to Upgrade when its record exists (and to the pending-install repair of ADR 0103 unit 5 when the CLI died mid-install), the install manifest and CR creates tolerate AlreadyExists, and the plan on the rerun sees a Cilium with an Ichor record in ichor-system, which is the first row of ADR 0103 1.1 ("already handled; adopt only checks the release exists").
1.8 Options considered and rejected.
- Cilium as a Talos inline manifest through the machine config, as
ichor initdoes. Rejected.initcan do it because it writes the config before the nodes exist. Here the cluster is already running,adoptnever writes to a node (ADR 0014, 0103), and the only other writer, the controller, cannot run without a pod network. Letting the CLI apply config to a live cluster just for this would add a node-writing exception to adopt. It would also leave an inline entry in a running cluster's machine config that has to be removed again by a second config change after the hand-over, and Talos's documented behaviour is that inline manifests are created and never updated (the Talos v1.14 Cilium guide), so the dependence of a post-bootstrap config change on a Kubernetes upgrade would have to be verified on hardware first. It would be two machine-config changes on a live cluster to reach the state step 2 reaches with none. - The CLI creates the
ichor-cniJob. Rejected. The Job is the controller's way to runApplywith cluster-admin-equivalent rights without making the long-lived controller cluster-admin (ADR 0099). The adopt CLI already holds the operator's credentials (it applies Ichor's CRDs and RBAC), so a Job adds a second place that builds the pod spec, a dependency on the installer image being pullable and onichor-cni's RBAC being applied first, and no extra safety. The cost of running in process is that a lost connection ends the wait;Applyis idempotent and the rerun continues, as in 1.7. - Adopt unmanaged and tell the operator to install a CNI. Rejected by the user's decision; and it would make Ichor refuse its most common "just give me a cluster" input.
- Cilium as a subchart of Ichor's own chart. Rejected for the Helm path as well (decision 4.4): it would put Cilium inside Ichor's chart release and so under two owners.
2. A cluster that already has a CNI
ADR 0103 stays as written: Cilium is taken over (1.2) or left alone with --cni-unmanaged; flannel, Calico and other CNIs are adopted only with --cni-unmanaged, as the unmanaged preset; a mixed CNI is refused. "Install with CNI management off" is that unmanaged mode and needs no change.
2.1 The D3 fix. The network-policy decision is made once, by installer.IncludeCiliumPolicies(detected, crdPresent) in writeInput, and nothing else may veto it. adoptWriter.installIchor stops passing the cluster's CNI preset to applyNetworkPolicies: when IncludeCiliumPolicies is true it calls it as a Cilium cluster (cniCilium), so the preset-based early return, which exists for ichor init --cni <other>, cannot skip the policies on a hands-off Cilium. When the gate is false adopt prints which input failed ("detected CNI is calico" or "the CiliumNetworkPolicy CRD is not Established"), as the installer already does. Tests: unmanaged-on-Cilium applies policies; unmanaged-on-flannel and -on-Calico do not; managed Cilium (takeover and the new no-CNI path) does. For the no-CNI path the gate is evaluated after step 2, when Cilium is the detected CNI.
2.2 status.cni.detected for the no-CNI path is cilium from the moment step 2 ends, so the controller's own gate (installer Job on an upgrade) agrees with adopt's.
3. Waits are bounded and say why
Three waits in adopt and init can currently go on until a 30-minute default (defaultWebhookWait) with only the last transport error printed: the webhook wait of createWhenWebhooksUp, the Cilium Ready wait before the policies, and adopt's waitHandover. They share one helper that:
- has a hard timeout per wait: webhooks 15 minutes in
adopt(30 ininit, unchanged, because a first install pulls images over a slow link), Cilium Ready 10, hand-over 10; the flag--wait-timeoutonadoptsets the webhook wait. On timeout the command exits non-zero and says what is left, never "waiting" forever; - every 30 seconds while waiting lists the pods in
ichor-systemand, for each one that is Pending, prints its scheduler condition message (for example "0/3 nodes are available: 3 node(s) had untolerated taint {node.kubernetes.io/not-ready}"), and for a pod inImagePullBackOfforCrashLoopBackOffthe waiting reason; - fails early when every Ichor pod has been unschedulable for two minutes and every node is NotReady with the network-plugin message: "Ichor's pods cannot start because this cluster has no pod network (its nodes are NotReady: cni plugin not initialized). Install a CNI, or let
ichor adoptinstall Cilium (a cluster with no CNI)". After decision 1 adopt cannot reach this state (it installs Cilium first and its inspection refuses to proceed on the other mixed states), so this guardsichor initagainst a broken Cilium, an adopt rerun against a cluster whose Cilium was removed, and the Helm path.
4. Installing Ichor with Helm
4.1 What the chart does and does not do. helm install of dist/chart installs Ichor, not a cluster: CRDs (kept on uninstall), the controller and its RBAC, the webhooks and their certificate, the UI, the optional Cilium policies, the ichor-cni identity. It cannot register a cluster, because that needs what only the operator's machine has and only the CLI does:
| Needs | Where |
|---|---|
| Running Ichor, CRDs, RBAC, UI, webhooks | Helm |
| Reading each node with the operator's talosconfig (versions, KubePrism, kube-proxy, install, drift) | CLI (ichor adopt) |
Recovering the secrets bundle (--recover-secrets) | CLI |
The plan, the consent, writing the TalosCluster, TalosMachines, held patches, and releasing the holds | CLI (the controller applies after release) |
| The CNI choice (Cilium takeover, unmanaged, or install Cilium on a no-CNI cluster) | CLI (ichor adopt, the same rules as decision 1 and 2; the chart has no CNI value for it) |
Registering a cluster from chart values (a TalosCluster templated in the chart) is rejected: it needs the node reads and the bundle, and a typed TalosCluster that was not checked against the live cluster is exactly the unreviewed drift the zero-drift rule exists to stop.
4.2 The sequence.
helm install ichor oci://ghcr.io/maeshinshin/charts/ichor --version X.Y.Z \
--namespace ichor-system --create-namespace [--set ...]
ichor adopt --cluster NAME --talosconfig FILE (--recover-secrets --recover-from ADDR | --secrets-bundle FILE) [--cni-unmanaged]
4.3 ichor adopt after a Helm install. ichorRunning becomes ichorPresent and answers true when the IchorInstallation singleton exists or a Helm release of the chart ichor is deployed (found with the Helm SDK's List over the release Secrets, any namespace) or the controller Deployment carrying the chart's selector label exists. If the install is Helm-managed:
- the install step is skipped entirely: no install manifest, no
IchorInstallationcreated (a chart-installed Ichor is upgraded through the chart,installer.enabled: false; anIchorInstallationwould invite the installer to fight Helm), and the plan says "Ichor is installed by Helm (release X, version Y); not touching it"; - adopt checks that Ichor's version equals the CLI's (the existing skew rule) and refuses a mismatch with the
helm upgradecommand; - the network-policy gate is evaluated and, since Helm owns
ichor-system, adopt does not apply the policies itself: when the gate is true and the release hasnetworkPolicy.cilium.enabledunset or false it prints "setnetworkPolicy.cilium.enabled=trueand runhelm upgrade". (Withnullthe chart renders none and nothing applies them, because there is no installer.) - A new flag
--require-ichor(default off) makes a missing Ichor an error ("install Ichor with Helm first") instead of installing it, for operators who want adopt never to install anything.
A controller or UI that needs the IchorInstallation object to exist must tolerate its absence; unit 5 checks this and, where something does need it, fixes that code rather than having the chart create the object.
4.4 The CNI rule on the Helm path is the same, and a no-CNI cluster cannot use the chart alone. The chart's pods need a pod network, so on a cluster with no CNI helm install --wait would hang the same way adopt did. Decided:
- A chart preflight (a named template using
lookupon Nodes,fails the install with a message; skipped byhelm template/--dry-run, wherelookupreturns nothing, and bypreflight.cni: false) refuses to install when every node is NotReady with the network-plugin message: "this cluster has no pod network, so Ichor's pods cannot start. Runichor cni bootstrapfirst (it installs Ichor's Cilium), thenhelm installagain." - A new command
ichor cni bootstrap --kubeconfig FILE --kube-proxy enabled|replaced [--tunnel vxlan|geneve]runs exactly the step 2 of 1.5 (the same function as adopt's): createsichor-systemif needed, installs the pinned Cilium as Ichor's release record there, waits bounded for the nodes to be Ready. The kube-proxy mode is explicit because the command has no Talos credential to read it, and KubePrism on 7445 is stated in its help as a requirement (adopt's check 1.3.2 still runs later and catches a mistake). The values are the same function of (tunnel, kube-proxy) as in 1.4. ichor adopt, run after that, finds a Cilium with an Ichor record inichor-system(row 1 of ADR 0103 1.1), reads tunnel and kube-proxy from the record and the Talos side, and agrees or refuses on a mismatch (the 1.5 refusals now apply, as for any existing Cilium).- The chart does not ship Cilium (no subchart, no pre-install hook that runs Helm): a Cilium inside the
ichorrelease would have two owners and Ichor'sUpgradewould fighthelm upgrade ichor. This keeps "Ichor owns Cilium as a Helm release" (CLAUDE.md): the release isciliuminkube-systemwith its record inichor-system, whichever path made it. - For a cluster that has a CNI, the chart path is unchanged:
helm install, thenichor adoptwith takeover or--cni-unmanaged. The chart never decides.
4.5 Publishing the chart. The chart is published as an OCI artifact oci://ghcr.io/maeshinshin/charts/ichor, next to the images (ghcr.io/maeshinshin/ichor-controller, ichor-ui, ichor-installer), by a release workflow on a version tag: helm package --version V --app-version V and helm push. This is the only publishing target (no Helm repository index to host). Prerequisites in the same change: values.yaml image defaults become the real repositories (ghcr.io/maeshinshin/ichor-controller, ghcr.io/maeshinshin/ichor-ui; appVersion already supplies the tag), and the chart test installs from the packaged archive. Until the first release is pushed, helm install ichor ./dist/chart from a checkout with --set manager.image.repository=... --set ichorUI.image.repository=... is the only path, and the guide says so. Helm itself is installed by the user (CLAUDE.md: tools come from nix develop; flake.nix gets helm if missing for CI and the e2e cases).
4.6 Upgrade and uninstall. A Helm-installed Ichor is upgraded with helm upgrade (CRDs are in templates/, so they upgrade with it); ichor commands that would run the installer Job refuse on a Helm-managed install and print the Helm command. helm uninstall keeps the CRDs and therefore every cluster object (crd.keep: true); it does not touch Cilium (a separate release).
What must change (work units)
Sizes: S under a day, M one to two days, L three or more. Rows run in parallel unless "Needs" says otherwise. No CRD field is added or removed by this ADR.
| # | Unit | Files | Size | Needs |
|---|---|---|---|---|
| 1 | Bounded waits: the shared helper (timeouts, pod diagnostics, early failure), used by createWhenWebhooksUp, the Cilium Ready wait, waitHandover; --wait-timeout; fake-clock tests with a Pending pod | internal/cli/init_talos.go, init_netpol.go, adopt_run.go, adopt_write.go, adopt.go, new internal/cli/wait_diag.go, tests | M | none |
| 2 | D3 fix: installIchor no longer passes the cluster preset to applyNetworkPolicies; the gate decides; the skip message names the failed input | internal/cli/adopt_write.go, init_netpol.go, adopt_write_test.go | S | none |
| 3 | No-CNI detection and preconditions (1.1, 1.3, 1.4), refusal of --cni-unmanaged there, plan text, mapping of kube-proxy from the Talos side; cni.name: none render byte-equality fixture | internal/cli/adopt_cni.go, adopt_k8s_facts.go, adopt_run.go, adopt_plan.go, adopt_cni_test.go, pkg/render test | M | none |
| 4 | Cilium-first install in the adopt CLI: extract the body of runCNIHelmApply into cilium.Bootstrap(ctx, restConfig, cluster, mode, wait) (namespace create, Apply, DaemonSet/node Ready wait); adopt order 1.5 (Cilium, Ichor, CRs, hand-over, release); the values-equality test; --tunnel; rerun idempotence; hand-over with no inline entry and no ichor-cni Job when the hash matches | pkg/cni/cilium/helm.go or new bootstrap.go, internal/cli/cni_helm.go, adopt_run.go, adopt_write.go, internal/controller/cnihandover_controller.go and tests | L | 1, 2, 3 |
| 5 | Helm-aware adopt: ichorPresent (IchorInstallation, Helm release of chart ichor, controller Deployment), skip install and IchorInstallation, version-skew refusal, policy advice, --require-ichor; check that controller and UI tolerate no IchorInstallation; installer-Job commands refuse on Helm-managed installs | internal/cli/adopt_write.go, adopt_run.go, internal/controller/ichorinstallation_controller.go (read only paths), internal/uiapi, tests | M | 1 |
| 6 | ichor cni bootstrap command (calls unit 4's function); chart preflight template with lookup and preflight.cni value; chart test for both | new internal/cli/cni_bootstrap.go, dist/chart/templates/_preflight.tpl, dist/chart/templates/NOTES.txt, dist/chart/values.yaml, dist/chart/chart_test.go | S | 4 |
| 7 | Chart publishing: image defaults, release workflow (helm package/helm push to GHCR on a tag), chart install-from-archive test, helm in flake.nix if missing | dist/chart/values.yaml, .github/workflows/release-chart.yml, flake.nix, Makefile | M | none |
| 8 | Docs: architecture Bootstrap and CNI paragraphs, docs/guides/adopt-and-reclaim.md (no-CNI row, Helm path), new Helm install guide section, ADR 0103 amendment line, make adr-index | docs/design/architecture.md, docs/guides/, docs/adr/0103-*.md | S | all |
| 9 | Hardware cases E1-E7 below and the cluster generators (a cni.name: none cluster; a Helm-installed Ichor) | test/e2e | L | 1-7 as they land |
Unit 7, as built (images): .github/workflows/release-images.yml runs on the same v* tag and calls make demo-images VERSION=<tag> REGISTRY=ghcr.io CRANE_INSECURE=, the crane build that already produced the controller, UI and installer images, so the release images are ghcr.io/maeshinshin/{ichor-controller,ichor-ui,ichor-installer}:<tag>: the chart defaults, the install manifest and the IchorInstallation installerRepository default needed no change. linux/amd64 only, as the Makefile builds today; multi-arch is deferred. images.sh now reads CRANE_INSECURE (default --insecure, for the local demo registry).
Parallelism: 1, 2, 3 and 7 start together (disjoint files except adopt_run.go, which units 1 and 3 touch in different functions; land 1 first). 4 follows them; 5 starts after 1 and runs beside 4; 6 follows 4. Per the batch rule one hardware run covers units 1-6 integrated.
Hardware test plan
Status: run (2026-10-30), see "Hardware results". Same harness, budget and recording rules as ADR 0103 (control plane 2560Mi-3Gi, worker 1536Mi, at most 10 nodes; fixtures built with talosctl, one run per integrated batch in its own worktree; failing test names recorded even when a rerun passes). This group replaces D5 of ADR 0103.
E. No-CNI adoption
- E1 (replaces D5) 1 control plane plus 1 worker,
cluster.network.cni.name: none, KubePrism default, kube-proxy enabled.ichor adopt(managed): Cilium installed with its record inichor-system, nodes Ready, Ichor pods Running,CNIHandover=Truewith no inline entry and noichor-cniJob, CNI-relevant machine-config paths byte-identical before and after the first render, holds released,ichor cluster statussays managed Cilium. - E2 Same with kube-proxy disabled in Talos:
replaced,kubeProxyReplacement=true, Services work. - E3
--cni-unmanagedon a no-CNI cluster: refused, config hashes andichor-systemunchanged. - E4 KubePrism disabled: refused with the "enable KubePrism" message, nothing written. A node without a pod CIDR: refused.
- E5 Kill the CLI during the Helm install and again after the Cilium wait; rerun; assert idempotent finish (and the
pending-installrepair). - E6 Bounded wait: break the Ichor image (bad tag) on an otherwise good cluster and a cluster whose Cilium is removed after step 2; assert the command exits within the timeout with the Pending reason printed, and that the "no pod network" early failure fires in the second.
- E7 The D3 regression: flannel (no policies, notice printed), Calico (same) and Cilium adopted with
--cni-unmanaged(policies applied) and managed Cilium takeover (policies applied).
F. Helm path
- F1 On a cluster with Cilium:
helm installof the chart (from the packaged archive, then from GHCR after the first release);ichor adoptskips the install step, creates noIchorInstallation, registers the cluster, hold released;helm upgradethen works andhelm uninstallkeeps the CRDs and the cluster objects. - F2 On a flannel cluster:
helm install,ichor adopt --cni-unmanaged; the policy advice is printed and nothing is applied toichor-system. - F3 On a no-CNI cluster:
helm installis refused by the preflight with the message;ichor cni bootstrap --kube-proxy enabled,helm install,ichor adopt: Cilium row 1 is recognised, tunnel and kube-proxy read back, adoption completes. - F4 Chart/CLI version skew refused with the
helm upgradecommand;--require-ichorwith no Ichor refused. - F5
helm templateandhelm install --dry-runon a no-CNI cluster do not fail on the preflight.
Record the exact refusal and wait texts and the time of each phase, and write down what worked.
Hardware results (2026-10-30)
- E1, E3, E4, E7 passed. E2 passed on rerun: the first run failed with EPERM, fixed by the bounded webhook wait.
- Rerun of 2026-10-30 (images
v0.0.1284-g46458e2, Talos v1.14, 1 control plane + 1 worker, Cilium 1.18.0,--tunnel geneve):- E2 passed again end to end (adopt 4m32s; kube-proxy
replaced, Services resolve,KubeProxyReplacement: True). - E6 bad image passed: adopt exited 4m12s after it started waiting with
Ichor's webhooks did not finish within 4m0s (pod ... Pending; ... container manager waiting: ImagePullBackOff ...). The first run (Cilium installed 04:49:14, wait began 04:49:28) was cut off by the test's own 9m timeout, not by adopt; the rerun on that cluster printed the same refusal, andTestE6BadImageBoundedWaitfailed that rerun only on its "got past the Cilium step" assertion, because Cilium was already there. - E6 no pod network passed: Cilium wiped at 05:05:57, adopt failed 05:07:57 with
the pods of Ichor cannot start because this cluster has no pod network (its nodes are NotReady: cni plugin not initialized). Install a CNI, or letichor adoptinstall Cilium. - E5 kill during the Helm install (killed 22s after "installing Cilium") is only partly fixed. The record is left
pending-install. The rerun with--recover-secrets --tunnel geneveis now accepted (no bundle refusal, no tunnel refusal), installs Ichor and creates the TalosCluster. Butichor adoptdoes not touch the record when Cilium is already there, and the controller'sichor-cniJob fails withcilium: upgrading Helm release cilium: another operation (install/upgrade/rollback) is in progress:repairPendingrepairs only a record older than the apply timeout (10 minutes) because nothing setsApplyOptions.Exclusive. After the Job fails the controller does not requeue (CNIHandover=False JobFailed, no new Job for 21 minutes), so the record never gets repaired and the hand-over never completes. Open: setExclusivewhere the CNI lock is held (the Job), and/or repair in adopt's rerun path, and requeue after a failed Job. - Failing tests seen:
TestE5KillDuringHelmInstall(pending record,CNIHandoverFalse),TestE6BadImageBoundedWait(the aborted first run, then the Cilium-step assertion on a reused cluster); the harness'snoCNIJobhelper recursed into itself and is fixed.
- E2 passed again end to end (adopt 4m32s; kube-proxy
- Third run of 2026-10-30 (images
v0.0.1292-g26da779, with the pending-record repair; Talos v1.14.3, 1 control plane + 1 worker, Cilium 1.18.0,--tunnel geneve; each case on its own fresh no-CNI cluster, a cluster takes about 4m30s to come up):- E5 passed (
TestE5KillDuringHelmInstall, 530s). Killed 20s after "adopt: installing Cilium" (05:42:56): recordichor-system/sh.helm.release.v1.cilium.v1 status=pending-install, Cilium DaemonSets 0 ready. The rerun with--recover-secrets --tunnel genevewas accepted and printedadopt: Ichor's Cilium release record was left pending by an interrupted install; repairing it, theninstalling Cilium 1.18.0,Cilium is installed and the nodes are Ready2m15s later, created the Secret, TalosCluster and both TalosMachines and released both holds; the rerun took 4m34s. Records afterwards:cilium.v1 superseded,cilium.v2 deployed(an upgrade, no uninstall).CNIHandoverTrue, nodes Ready, controller Running. - E6 bad image passed (
TestE6BadImageBoundedWait, 464s, one run on a fresh cluster): adopt took 7m18s in all (Cilium install, then the 4m wait) and exited withIchor's webhooks did not finish within 4m0s (pod ichor-system/ichor-controller-manager-... Pending; ... container manager waiting: ImagePullBackOff; pod ichor-system/ichor-ichor-ui-... Pending; ... container ichor-ui waiting: ImagePullBackOff). - Failing tests seen in this run: none.
- E5 passed (
- E8 and E9 do not exist: the plan defines E1 to E7 only.
- F1 to F5 passed (F3 after the read-back fix a848ed73).
Consequences
- ADR 0103's no-CNI row, test D5 and open question 3 are superseded;
talosCNI: nonestays valid for clusters whose own CNI is applied by hand. - A new path writes to a cluster before any Ichor object exists: the adopt CLI installs Cilium with the operator's credential, announced in the plan and part of consent. It adds one node-visible change (Cilium becomes the cluster's CNI) but no machine-config change, and no write to a node.
- Adopt now has two orders: Cilium-first for a no-CNI cluster, install-first otherwise. The shared code is
cilium.Bootstrap. ichor initand adopt can no longer hang silently on a missing pod network.- A hands-off Cilium cluster gets Ichor's policies at adopt, as ADR 0103 said it should.
- Ichor becomes installable by Helm, with the chart published to GHCR; the install manifest and
IchorInstallationstay theinitpath. Two ways to install means two upgrade stories (Helm vs the installer Job), kept apart byichorPresent; the CLI refuses to mix them. - No CRD change. New CLI surface:
ichor cni bootstrap,ichor adopt --wait-timeout --tunnel --require-ichor.
Risks
- Cilium installed by a laptop process. A dropped connection leaves a
pending-installrecord or half-ready DaemonSet. Mitigated by idempotentApply, the repair path (ADR 0103 unit 5) and case E5; the user still has to rerun. - The values the CLI installs differ from the ones the controller derives, so the controller starts an
ichor-cniJob that restarts agents on a live network. Mitigated by building theTalosClusterfirst, the equality test and case E1's "no Job" assertion. - KubePrism assumed. A no-CNI cluster with KubePrism off is refused rather than fixed; an operator on an old config has one extra manual step. The refusal text gives it.
- Detection of "no CNI" misreads a cluster whose CNI is broken (all nodes NotReady because the user's Calico crashed). The rule requires no CNI workload to exist; a crashed Calico DaemonSet exists, so that cluster is the Calico (unmanaged) case, not this one.
- A Helm-installed Ichor with no
IchorInstallationmay hit code that assumes it. Unit 5 audits and fixes; case F1 covers it. - The chart has never been published, and its values defaults were placeholders. Until unit 7 lands the Helm path is checkout-only; this ADR does not hide that.
- Hand-over with no inline entry is a path the hand-over controller has not run on hardware. Case E1 is its first run.
As built (2026-10-29)
Units 1 to 7 are built; unit 9 (hardware cases) was run on 2026-10-30, see "Hardware results". Where the build differs from the text above, the build is right:
- The Cilium Helm Job uses
ApplyForCluster, notBootstrap.cilium.Bootstrapis the CLI's entry (namespace,Apply, Ready wait); theichor-cniJob and the CLI shareApplyForClusterunderneath. The Job also runs without anIchorInstallation, which a Helm install does not have. ichor cni bootstrapcannot check KubePrism. It has no Talos credential, so it states the requirement in its help and relies on adopt's later check (1.3.2).--kube-proxy enabled|replacedis required for the same reason.- Strict version equality for Helm installs. Adopt on a Helm-installed Ichor refuses any version different from the CLI's, not only a skew beyond the usual rule, and prints the
helm upgradecommand. - Chart version has no leading
v;appVersionand image tags have it (for example chart0.1.0, appVersion and tagv0.1.0). - Images are linux/amd64 only. They are built by the existing crane target (
make demo-images) on av*tag, asghcr.io/maeshinshin/{ichor-controller,ichor-ui,ichor-installer}. - The UI server's Helm detection is limited. The Ichor upgrade path in the UI points to
helm upgradewhen it can tell the install is Helm-managed, but it cannot detect every Helm install, so some prompts may still describe the installer Job. - No release exists yet.
oci://ghcr.io/maeshinshin/charts/ichorand the image repositories are empty until the firstv*tag is pushed. - Added flags beyond the text:
ichor adopt --cilium-values FILE(open question 1) andichor cni bootstrap --cilium-values FILE,--tunnel.
Guides: docs/guides/install-with-helm.md, and the no-CNI and Helm sections of docs/guides/adopt-and-reclaim.md.
Open questions
All three are resolved.
--cilium-values FILEfor the no-CNI path: accepted and built, onichor adoptandichor cni bootstrap. Ichor-owned keys are refused.ichor cni bootstrapforichor init: no.initkeeps the inline-manifest bootstrap;ichor cni bootstrapis for an existing cluster with no pod network.- Default timeouts: kept as built (15 minutes for the webhooks in adopt, 30 in
init, 10 for Cilium Ready, 10 for hand-over). Hardware cases E6 and the first real runs tune them.
Amendment 2026-10-29 (ADR 0105): a Helm install does get an IchorInstallation: the controller creates it once when absent (--deployed-by helm, set by the chart), with spec.deployment.manager: helm; the chart still never renders it. ichorPresent keeps its three signals, but spec.deployment.manager is authoritative once the object exists. Adopt skips the install step on manager: helm and does not wait for the object; installer-Job commands and a TalosOperation of kind ichor are refused on Helm. The chart adds ichorUI.port (8443), ichorUI.plaintextPort (opt-in, deprecated), ichorUI.tls.caConfigMap/caSecret/caKey and settings.readOnly.
Amendment (2026-11-04): release artifacts and the version stamp
appVersion carries the leading v (v0.1.0) because the chart's default image tag is .Chart.AppVersion and images are tagged with the git tag; version stays without the v. make release VERSION=vX.Y.Z is the local equivalent of the release workflow's build job and writes the CLI, the install manifest and the chart to bin/release/. The goreleaser build uses -tags installembed with the install manifest generated in a before hook for the tag being released, and stamps {{ .Tag }} (not {{ .Version }}) so the version keeps its v and passes semver.IsValid.