0055. Cilium stays Synced under Argo CD with ignoreDifferences on CiliumIdentity and ServerSideApply
Status: Accepted (amended 2026-10-26); superseded in part by ADR 0099 · Date: 2026-10-26 · Design section: CNI, GitOps and Argo CD
Decided: The generated Cilium Application carries ignoreDifferences: [{group: cilium.io, kind: CiliumIdentity}] and ServerSideApply=true, without RespectIgnoreDifferences=true; Hubble certificates stay on hubble.tls.auto.method=cronJob; Kokumi cannot carry either setting today, so that case is an open question; the generated Cilium and root Applications use automated sync with prune and selfHeal (addendum 2026-10-26)
Context
The design's last open question was which Cilium and Calico values avoid a
permanent OutOfSync under Argo CD. The user chose ignoreDifferences and named
a reference: their own homelab Argo CD, read-only on 2026-10-26.
What was observed there (the user's cluster, not a cluster Ichor handed over):
Application cilium, Cilium chart 1.20.0, status Synced with selfHeal: true, syncOptions: [ServerSideApply=true], and ignoreDifferences with the
single entry {group: cilium.io, kind: CiliumIdentity}. argocd-cm held no
Cilium-specific customization. Its values were ipam.mode: kubernetes,
kubeProxyReplacement: true, securityContext.capabilities, cgroup,
k8sServiceHost: localhost, k8sServicePort: 7445, plus homelab-only choices
(Gateway API, L2 announcements, Hubble UI service type and metrics) and no
hubble.tls.auto.method.
Decision
Argo CD on its own (Ichor writes the Application, pkg/cni/cilium): the
Application gets the one ignoreDifferences entry and keeps
ServerSideApply=true and CreateNamespace=true. Release name (cilium),
namespace (kube-system) and values are unchanged from the bootstrap
manifest, so ADR 0024's no-diff hand-over still holds; a test now compares the
two byte for byte.
RespectIgnoreDifferences=true is not added. Argo CD's sync-options
documentation (argo-cd.readthedocs.io, stable, read 2026-10-26; the page names
no version) says ignoreDifferences is by default used only to compute sync
status, and that the option makes a sync pre-patch the desired state so
ignored fields are not applied over live ones; it is only effective when the
resource already exists. CiliumIdentity objects are created by the Cilium
operator and appear nowhere in the chart (no template under
install/kubernetes/cilium/templates at tag v1.20.0 mentions the kind), so
there is no desired manifest to pre-patch. The reference has no such option and
is Synced. gitops.New therefore changed: it adds the option only when an
entry names fields (jsonPointers, jqPathExpressions or
managedFieldsManagers), which is the case ADR 0015 introduced the pairing
for. A kind-only entry gets no option. Still one constructor.
Hubble certificates stay cronJob. Read in the Cilium v1.20.0 chart:
hubble.tls.auto.method defaults to helm. The tls-helm templates call
genCA/genSignedCert on every render, try to reuse existing material through
lookup (which helm template, what Argo CD runs, cannot do because it has no
cluster access), and label each Secret cilium.io/helm-template-non-idempotent: "true". So under Argo CD a re-render yields new certificates. The reference
runs the default and is Synced; the likely reason is that Argo CD's repo-server
caches rendered manifests per revision, so a re-render only happens on a new
revision or hard refresh. That reason was not tested. cronJob renders no
random material (the Job and CronJob generate certificates in the cluster), so
the diff cannot arise and the behaviour does not depend on cache timing. It is
also the same value in the bootstrap manifest and the Application. Kept.
Values compared with the reference. All five Talos essentials are already
present, with identical content: ipam.mode: kubernetes;
kubeProxyReplacement; securityContext.capabilities (ciliumAgent,
cleanCiliumState); cgroup.autoMount.enabled: false and hostRoot: /sys/fs/cgroup; k8sServiceHost: localhost and k8sServicePort: 7445
(these two only when kube-proxy is replaced, as in Talos's guide). Nothing was
added. Not added: gatewayAPI, l2announcements, bgpControlPlane, Hubble
relay/UI/metrics, which are homelab choices.
Chart version. The reference runs 1.20.0; Ichor pins 1.18.0
(ChartVersion). The observation is of a newer chart than the one Ichor
installs. The pin is not bumped here: ADR 0013 ties the bump to a review of the
values against the new chart. Whether the entry behaves the same on 1.18.0 is
checked in the e2e run.
Caveat. Neither the Argo CD docs for ignoreDifferences (diffing guide,
read 2026-10-26) nor the observation explain mechanically how a group/kind-only
entry affects the result; every documented example names a field, and the page
does not say what a kind-only entry does. The decision rests on the observed
Synced state, not on a derived mechanism. Ichor's own confirmation is the e2e
on a cluster Ichor handed over; until then this is a copy of what worked
elsewhere.
Argo CD alone versus Argo CD with Kokumi
| Argo CD alone | Argo CD with Kokumi (ADR 0048, design only) | |
|---|---|---|
| Who writes the Application | Ichor (pkg/cni/cilium) | Kokumi's Serving controller |
ServerSideApply=true | Ichor sets it | Kokumi already sets it (hard-coded) |
ignoreDifferences | Ichor sets it on the Application | No field exists to pass it |
RespectIgnoreDifferences | not needed | not needed, same reason |
| Where the ignore can live instead | n/a | argocd-cm resource.customizations.ignoreDifferences.cilium.io_CiliumIdentity, shipped as a user-merged ConfigMap like ADR 0046 |
| Status | implemented | open question |
Source for the Kokumi column: kokumi-dev/kokumi at commit 4126144 (2026-10-21):
internal/argocd/application.go builds the Application as a literal with
syncPolicy.automated {prune, selfHeal: true} and syncOptions: ["ServerSideApply=true"] and no ignoreDifferences; no api/v1alpha1 type
carries Application fields; applyApplication in
internal/controller/serving_controller.go compares the whole spec and
updates the Application when it differs, so an edit to the Application is
reverted. kokumi.dev's overview page says nothing on the subject. This is a
reading of the source of an experimental v1alpha1 project, not a statement of
its contract. The CiliumIdentity problem exists with Kokumi too: Argo CD
compares the artifact's rendered manifests with live regardless of who made the
Application.
What is not decided: whether the argocd-cm route works. Its documented form
(resource.customizations.ignoreDifferences.<group>_<kind>) takes
jsonPointers, jqPathExpressions or managedFieldsManagers, and a
kind-only entry is not documented there, so the equivalent of the Application
entry is unknown. Ichor does not write argocd-cm (ADR 0046).
Calico
There is no reference. Calico is phase 11. Its values are decided when it is built, the same way: observe a working Argo CD-managed Calico.
Consequences
gitops.Newno longer addsRespectIgnoreDifferences=truefor kind-only entries (ADR 0015 amended).- The design's Open questions shrinks to Calico and the Kokumi case.
- Chart 1.18.0 versus the 1.20.0 observation is an explicit gap for e2e.
E2E observation (2026-10-26)
Run on a throwaway 3-control-plane Talos v1.14.3 cluster (ichor-e2e
harness, ICHOR_E2E_CNI=none), Kubernetes v1.37.1. Cilium chart 1.18.0 was
installed from helm template with the values pkg/cni/cilium produces,
applied with server-side apply (the stand-in for the inline manifest: today
BootstrapManifest returns only the values document, see ADR 0013). Argo CD
v3.5.6 core-install.yaml was installed on the inner cluster only. The
Application is the one ArgoCDApplication returns, with one added value
(tunnelProtocol: geneve, below) in both the install and the Application.
- Before the first sync: OutOfSync, 25 resources, and the only difference in
every one is the
argocd.argoproj.io/tracking-idannotation (fromargocd app diff --core; no other line differed). That includes the HubblecronJobresources, so the chosen method produces no render difference. The hand-over is "no diff" in content, but not Synced until Argo CD writes its tracking annotation, which needs one sync. - The Ichor Application has no
syncPolicy.automated. Argo CD never syncs it by itself. One manual sync (13:23 JST) succeeded and the Application was Synced/Healthy; the DaemonSet did not change generation and no Cilium pod restarted. It stayed Synced for the 15 minutes watched (13:23 to 13:38; the last reconcile was 13:38:12) with no further operation. - With
automated: {prune: true, selfHeal: true}(as in the homelab) added to the same Application: Synced/Healthy for another 10 minutes (13:40 to 13:50), no new sync operation, no restarts. - Isolation: with
ignoreDifferencesremoved (checked:spec.ignoreDifferencesempty), with selfHeal on: still Synced/Healthy for 5 minutes (13:46 to 13:50) with 9CiliumIdentityobjects present. So on this cluster the kind-only entry was not needed, and nothing was shown about what it does. Consistent with the Context above: the chart renders noCiliumIdentity, and they are cluster-scoped objects Argo CD does not track. The entry is kept (homelab reference). Not tested: a Cilium upgrade, or features that create more operator-owned objects. - Not exercised: the
CNIHandovercontroller (cluster set tohandover: disabled) and a Talos-inlined bootstrap manifest.
Things the run turned up:
- Argo CD
core-install.yamlhas nodefaultAppProject; the Application reportedInvalidSpecErroruntil one was created. - VXLAN (UDP 8472) between the nodes is dropped on
talos-cluster01: Cilium's default tunnel gave1/3 reachable, pod to service and pod DNS timed out, and Argo CD's repo-server could not resolve anything. Switching totunnelProtocol: genevefixed it at once. Cause on the outer network not investigated. Any Cilium e2e on this harness needs it. - Chart 1.18.0 ran on Kubernetes v1.37.1 here without trouble in this short run.
Addendum (2026-10-26): generated Applications sync automatically
Decided by the user: with Argo CD in use, automated sync is fine; without Argo
CD this does not apply. The e2e run above showed that an Application without
syncPolicy.automated stays OutOfSync until someone syncs it, so the CNI
hand-over (ADR 0024) never completes.
Distinction. "Ichor never touches an Application's sync policy" means Ichor
never changes the sync policy of an Application that already exists. An
Application Ichor generates (only on the Argo CD / hand-over path) chooses its
sync policy once, at creation, in pkg/gitops.New (ADR 0015).
Every generated Application (there are no others; gitops export reuses
these two builders, flannel's ArgoCDApplication returns nil):
| Application | Built by | Policy |
|---|---|---|
cilium (CNI) | pkg/cni/cilium | automated {prune: true, selfHeal: true}, ServerSideApply=true, CreateNamespace=true, PruneLast=true |
root (app of apps) | internal/cli.buildRootApplication | automated {prune: true, selfHeal: true} (already so; unchanged) |
Prune, per Application (Argo CD v3.5 docs, sync-options and auto_sync):
- CNI:
prune: true. Prune removes only resources Argo CD tracks that the pinned chart no longer renders, i.e. after a deliberate chart or value change.prune: falsewould not stop that change; it would leave the app OutOfSync ("the app will be out of sync if Argo CD expects a resource to be pruned"), which blocks the Synced + Healthy signal the hand-over waits for. The risk that a dropped resource takes the network down is reduced byPruneLast=true(pruning runs as a last implicit wave, after the new resources are healthy). A user can protect any single resource with theargocd.argoproj.io/sync-options: Prune=falseannotation, which overrides the policy. - root:
prune: true. It deletes a child Application only when its manifest is removed from Git, which is the intent. Ichor sets noresources-finalizeron children, so deleting a child Application does not cascade-delete the workloads it deployed.
Without --argocd (or with handover: disabled) no Application is generated and
nothing changes; TestBuildCNIBootstrap_Cilium and _Flannel check it.