Skip to content

Add support for drift detection ignore rules#1627

Open
dipti-pai wants to merge 1 commit into
fluxcd:mainfrom
dipti-pai:drift-ignore-rules
Open

Add support for drift detection ignore rules#1627
dipti-pai wants to merge 1 commit into
fluxcd:mainfrom
dipti-pai:drift-ignore-rules

Conversation

@dipti-pai
Copy link
Copy Markdown
Member

@dipti-pai dipti-pai commented Mar 31, 2026

Changes include:

Fix: #1138

Test summary with kustomize-controller

E2E Test Results — DriftIgnoreRules

Setup

$ kubectl get gitrepository -n drift-verbose-setup
drift-test-repo   https://github.com/dipti-pai/flux-test-repo   2026-05-12T16:37:21Z   True   stored artifact for revision 'main@sha1:b2a96870abdf6a295501e298a147e1e4ea5d878a'

Source manifests (in ssaDriftIgnoreRulesTest/) deploy:

  • ConfigMap test-configmanaged-key: "v1", ignored-key: "original", another-ignored: "keep-me"
  • Deployment test-nginxnginx:1.25, 1 replica, with resource requests/limits
  • Service test-service — with annotation external-dns.alpha.kubernetes.io/hostname: "test.example.com"

Reconciliation verification approach

Each test triggers reconciliation after introducing drift, checks the controller
logs and confirms non-ignored fields are corrected while ignored fields are preserved.

Test 1 — Ignore /spec/replicas on Deployments (HPA use case)

Kustomization:

driftIgnoreRules:
- paths:
    - "/spec/replicas"
  target:
    kind: Deployment

Initial state:

$ kubectl get ks -n drift-verbose-1
NAME                AGE   READY   STATUS
test-basic-ignore   3s    True    Applied revision: main@sha1:b2a96870abdf6a295501e298a147e1e4ea5d878a

$ kubectl get deploy test-nginx -n drift-verbose-1 -o jsonpath='{.spec.replicas}'
1

Simulate HPA scaling + image tamper (server-side apply with hpa-controller field manager):

$ kubectl apply --server-side --field-manager=hpa-controller --force-conflicts -f deployment-scaled.yaml
deployment.apps/test-nginx serverside-applied

$ kubectl get deploy test-nginx -n drift-verbose-1 -o jsonpath='{.spec.replicas}'
3

$ kubectl get deploy test-nginx -n drift-verbose-1 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.24

After reconciliation — resource verification:

$ kubectl get deploy test-nginx -n drift-verbose-1 -o jsonpath='{.spec.replicas}'
3  # <-- replicas preserved!

$ kubectl get deploy test-nginx -n drift-verbose-1 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.25  # <-- image corrected!

Controller log:

{
    "level": "info",
    "ts": "2026-05-12T16:37:45.163Z",
    "msg": "server-side apply completed",
    "controller": "kustomization",
    "controllerGroup": "kustomize.toolkit.fluxcd.io",
    "controllerKind": "Kustomization",
    "Kustomization": {
        "name": "test-basic-ignore",
        "namespace": "drift-verbose-1"
    },
    "namespace": "drift-verbose-1",
    "name": "test-basic-ignore",
    "reconcileID": "b9d220b6-7b6c-404f-a7e1-d0f2a9c07eb7",
    "output": {
        "ConfigMap/drift-verbose-1/test-config": "unchanged",
        "Deployment/drift-verbose-1/test-nginx": "configured",
        "Namespace/drift-verbose-1": "unchanged",
        "Service/drift-verbose-1/test-service": "unchanged"
    },
    "revision": "main@sha1:b2a96870abdf6a295501e298a147e1e4ea5d878a"
}

Test 2 — Targeted selectors (external-dns + ConfigMap field)

Kustomization:

driftIgnoreRules:
- paths:
    - "/metadata/annotations/external-dns.alpha.kubernetes.io~1hostname"
  target:
    kind: Service
    name: test-service
- paths:
    - "/data/ignored-key"
  target:
    kind: ConfigMap
    name: test-config

Initial state:

$ kubectl get svc test-service -n drift-verbose-2 -o jsonpath='{.metadata.annotations}'
{"external-dns.alpha.kubernetes.io/hostname":"test.example.com"}

$ kubectl get cm test-config -n drift-verbose-2 -o jsonpath='{.data}'
{"another-ignored":"keep-me","ignored-key":"original","managed-key":"v1"}

Simulate external-dns and controller changes (server-side apply):

$ kubectl apply --server-side --field-manager=external-dns --force-conflicts -f svc-modified.yaml
service/test-service serverside-applied

$ kubectl apply --server-side --field-manager=config-controller --force-conflicts -f cm-modified.yaml
configmap/test-config serverside-applied

$ kubectl get svc test-service -n drift-verbose-2 -o jsonpath='{.metadata.annotations}'
{"external-dns.alpha.kubernetes.io/hostname":"production.example.com"}

$ kubectl get cm test-config -n drift-verbose-2 -o jsonpath='{.data}'
{"another-ignored":"keep-me","ignored-key":"changed-by-controller","managed-key":"tampered"}

After reconciliation — resource verification:

$ kubectl get svc test-service -n drift-verbose-2 -o jsonpath='{.metadata.annotations}'
{"external-dns.alpha.kubernetes.io/hostname":"production.example.com"}  # <-- annotation preserved!

$ kubectl get cm test-config -n drift-verbose-2 -o jsonpath='{.data}'
{"another-ignored":"keep-me","ignored-key":"changed-by-controller","managed-key":"v1"}  # <-- ignored-key preserved, managed-key corrected!

Controller log:

{
    "level": "info",
    "ts": "2026-05-12T16:38:27.539Z",
    "msg": "server-side apply completed",
    "controller": "kustomization",
    "controllerGroup": "kustomize.toolkit.fluxcd.io",
    "controllerKind": "Kustomization",
    "Kustomization": {
        "name": "test-targeted-ignore",
        "namespace": "drift-verbose-2"
    },
    "namespace": "drift-verbose-2",
    "name": "test-targeted-ignore",
    "reconcileID": "b890bdfe-aec9-4418-b5ce-8c7f8a4c6c93",
    "output": {
        "ConfigMap/drift-verbose-2/test-config": "configured",
        "Deployment/drift-verbose-2/test-nginx": "unchanged",
        "Namespace/drift-verbose-2": "unchanged",
        "Service/drift-verbose-2/test-service": "unchanged"
    },
    "revision": "main@sha1:b2a96870abdf6a295501e298a147e1e4ea5d878a"
}

Test 3 — Multi-resource (Deployment replicas + Service annotations + ConfigMap fields)

Kustomization:

driftIgnoreRules:
- paths:
    - "/spec/replicas"
  target:
    kind: Deployment
- paths:
    - "/metadata/annotations"
  target:
    kind: Service
- paths:
    - "/data/ignored-key"
    - "/data/another-ignored"
  target:
    kind: ConfigMap

Out-of-band changes via server-side apply (3 different field managers):

$ kubectl apply --server-side --field-manager=hpa-controller --force-conflicts ...     # replicas: 3, image: 1.24
$ kubectl apply --server-side --field-manager=external-dns --force-conflicts ...       # added annotation
$ kubectl apply --server-side --field-manager=config-controller --force-conflicts ...  # changed CM keys

Before reconciliation:

$ kubectl get deploy test-nginx -n drift-verbose-3 -o jsonpath='{.spec.replicas}'
3

$ kubectl get deploy test-nginx -n drift-verbose-3 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.24

$ kubectl get svc test-service -n drift-verbose-3 -o jsonpath='{.metadata.annotations}'
{"external-dns.alpha.kubernetes.io/hostname":"test.example.com","my-custom-annotation":"my-value"}

$ kubectl get cm test-config -n drift-verbose-3 -o jsonpath='{.data}'
{"another-ignored":"y","ignored-key":"x","managed-key":"tampered"}

After reconciliation — resource verification:

$ kubectl get deploy test-nginx -n drift-verbose-3 -o jsonpath='{.spec.replicas}'
3  # <-- preserved

$ kubectl get deploy test-nginx -n drift-verbose-3 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.25  # <-- corrected

$ kubectl get svc test-service -n drift-verbose-3 -o jsonpath='{.metadata.annotations}'
{"external-dns.alpha.kubernetes.io/hostname":"test.example.com","my-custom-annotation":"my-value"}  # <-- preserved

$ kubectl get cm test-config -n drift-verbose-3 -o jsonpath='{.data}'
{"another-ignored":"y","ignored-key":"x","managed-key":"v1"}  # <-- ignored fields preserved, managed-key corrected

Controller log:

{
    "level": "info",
    "ts": "2026-05-12T16:39:10.689Z",
    "msg": "server-side apply completed",
    "controller": "kustomization",
    "controllerGroup": "kustomize.toolkit.fluxcd.io",
    "controllerKind": "Kustomization",
    "Kustomization": {
        "name": "test-multi-resource",
        "namespace": "drift-verbose-3"
    },
    "namespace": "drift-verbose-3",
    "name": "test-multi-resource",
    "reconcileID": "2bfb5e95-0a12-40d0-94ce-6d7e0a0295dd",
    "output": {
        "ConfigMap/drift-verbose-3/test-config": "configured",
        "Deployment/drift-verbose-3/test-nginx": "configured",
        "Namespace/drift-verbose-3": "unchanged",
        "Service/drift-verbose-3/test-service": "unchanged"
    },
    "revision": "main@sha1:b2a96870abdf6a295501e298a147e1e4ea5d878a"
}

Test 4 — Nested paths (VPA container resources)

Kustomization:

driftIgnoreRules:
- paths:
    - "/spec/template/spec/containers/0/resources"
  target:
    kind: Deployment
    name: test-nginx

Initial state:

$ kubectl get deploy test-nginx -n drift-verbose-4 -o jsonpath='{.spec.template.spec.containers[0].resources}'
{"limits":{"cpu":"200m","memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}}

Simulate VPA adjusting resources + image tamper (server-side apply with vpa-recommender field manager):

$ kubectl apply --server-side --field-manager=vpa-recommender --force-conflicts -f deployment-vpa.yaml
deployment.apps/test-nginx serverside-applied

$ kubectl get deploy test-nginx -n drift-verbose-4 -o jsonpath='{.spec.template.spec.containers[0].resources}'
{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"256Mi"}}

$ kubectl get deploy test-nginx -n drift-verbose-4 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.24

After reconciliation — VPA resources preserved, image corrected:

$ kubectl get deploy test-nginx -n drift-verbose-4 -o jsonpath='{.spec.template.spec.containers[0].resources}'
{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"256Mi"}}  # <-- VPA resources preserved!

$ kubectl get deploy test-nginx -n drift-verbose-4 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.25  # <-- image corrected!

Controller log:

{
    "level": "info",
    "ts": "2026-05-12T16:39:53.423Z",
    "msg": "server-side apply completed",
    "controller": "kustomization",
    "controllerGroup": "kustomize.toolkit.fluxcd.io",
    "controllerKind": "Kustomization",
    "Kustomization": {
        "name": "test-nested-paths",
        "namespace": "drift-verbose-4"
    },
    "namespace": "drift-verbose-4",
    "name": "test-nested-paths",
    "reconcileID": "650121b8-bc1c-46e1-98c7-981ec3901f8b",
    "output": {
        "ConfigMap/drift-verbose-4/test-config": "unchanged",
        "Deployment/drift-verbose-4/test-nginx": "configured",
        "Namespace/drift-verbose-4": "unchanged",
        "Service/drift-verbose-4/test-service": "unchanged"
    },
    "revision": "main@sha1:b2a96870abdf6a295501e298a147e1e4ea5d878a"
}

Test 5 — Edge cases (non-existent paths + non-matching selector)

Kustomization:

driftIgnoreRules:
- paths:
    - "/data/nonexistent-key"
    - "/spec/this/does/not/exist"
- paths:
    - "/data/ignored-key"
  target:
    kind: ConfigMap
    name: does-not-exist

Initial state — reconciles successfully despite non-existent paths:

$ kubectl get ks -n drift-verbose-5
NAME              AGE   READY   STATUS
test-edge-cases   4s    True    Applied revision: main@sha1:b2a96870abdf6a295501e298a147e1e4ea5d878a

Change ignored-key via SSA (rule targets does-not-exist CM, so should NOT match test-config):

$ kubectl get cm test-config -n drift-verbose-5 -o jsonpath='{.data.ignored-key}'
edge-changed

After reconciliation:

$ kubectl get cm test-config -n drift-verbose-5 -o jsonpath='{.data.ignored-key}'
original  # <-- reverted (selector targets "does-not-exist", not "test-config")

$ kubectl get cm test-config -n drift-verbose-5 -o jsonpath='{.data.managed-key}'
v1  # <-- corrected

No errors in controller logs:

$ kubectl -n kustomize-system logs deployment/kustomize-controller --since=2m | grep -i error | grep "test-edge-cases"
(no errors)

Controller log:

{
    "level": "info",
    "ts": "2026-05-12T16:40:33.786Z",
    "msg": "server-side apply completed",
    "controller": "kustomization",
    "controllerGroup": "kustomize.toolkit.fluxcd.io",
    "controllerKind": "Kustomization",
    "Kustomization": {
        "name": "test-edge-cases",
        "namespace": "drift-verbose-5"
    },
    "namespace": "drift-verbose-5",
    "name": "test-edge-cases",
    "reconcileID": "a17c71f0-c19a-4e64-bb71-4d5054085c1e",
    "output": {
        "ConfigMap/drift-verbose-5/test-config": "configured",
        "Deployment/drift-verbose-5/test-nginx": "unchanged",
        "Namespace/drift-verbose-5": "unchanged",
        "Service/drift-verbose-5/test-service": "unchanged"
    },
    "revision": "main@sha1:b2a96870abdf6a295501e298a147e1e4ea5d878a"
}

Test 6a — Ignore immutable /spec/selector + image drift

Kustomization:

driftIgnoreRules:
- paths:
    - "/spec/selector"
  target:
    kind: Deployment
    name: test-nginx

After introducing image drift and reconciling:

$ kubectl get ks test-selector-ignore -n drift-verbose-6a -o jsonpath='{.status.conditions[0].status}'
True  # <-- Ready

$ kubectl get deploy test-nginx -n drift-verbose-6a -o jsonpath='{.spec.selector}'
{"matchLabels":{"app":"nginx"}}  # <-- selector preserved

$ kubectl get deploy test-nginx -n drift-verbose-6a -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.25  # <-- image corrected

Test 6b — driftAdopt — ignored field preserved when Flux is sole Apply owner

Kustomization:

driftIgnoreRules:
- paths:
    - "/data/ignored-key"
  target:
    kind: ConfigMap
    name: test-config

Initial state:

$ kubectl get cm test-config -n drift-verbose-6b -o jsonpath='{.data}'
{"another-ignored":"keep-me","ignored-key":"original","managed-key":"v1"}

Drift BOTH fields via non-SSA patch (Update operation — no other Apply manager claims ignored-key):

$ kubectl patch cm test-config -n drift-verbose-6b --type merge -p '{"data":{"ignored-key":"adopted-value","managed-key":"tampered"}}'
configmap/test-config patched

$ kubectl get cm test-config -n drift-verbose-6b -o jsonpath='{.data}'
{"another-ignored":"keep-me","ignored-key":"adopted-value","managed-key":"tampered"}

After reconciliation — driftAdopt behavior:

$ kubectl get ks test-drift-adopt -n drift-verbose-6b -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
True

$ kubectl get cm test-config -n drift-verbose-6b -o jsonpath='{.data}'
{"another-ignored":"keep-me","ignored-key":"adopted-value","managed-key":"v1"}
# managed-key corrected to "v1", ignored-key preserved as "adopted-value" (driftAdopt)

Flux retains ownership of ignored-key (adopted into payload):

$ kubectl get cm test-config -n drift-verbose-6b -o json --show-managed-fields | jq '.metadata.managedFields[] | select(.manager == "kustomize-controller" and .operation == "Apply") | .fieldsV1'
# Contains "ignored-key" — Flux adopted the in-cluster value

Test 6c — driftAdopt on mandatory field (spec/replicas, Flux sole owner)

Kustomization:

driftIgnoreRules:
- paths:
    - "/spec/replicas"
  target:
    kind: Deployment
    name: test-nginx

Drift replicas + image via non-SSA patch:

$ kubectl get deploy test-nginx -n drift-verbose-6c -o jsonpath='{.spec.replicas}'
3

$ kubectl get deploy test-nginx -n drift-verbose-6c -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.24

After reconciliation:

$ kubectl get ks test-mandatory-adopt -n drift-verbose-6c -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
True

$ kubectl get deploy test-nginx -n drift-verbose-6c -o jsonpath='{.spec.replicas}'
3  # <-- preserved via driftAdopt

$ kubectl get deploy test-nginx -n drift-verbose-6c -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.25  # <-- corrected

Test 7 — No target selector — annotations ignored on ALL resources

Kustomization:

driftIgnoreRules:
- paths:
    - "/metadata/annotations"

Add custom annotations on ALL resource types via SSA + tamper image:

$ kubectl get cm test-config -n drift-verbose-7 -o jsonpath='{.metadata.annotations.custom/cm-note}'
cm-annotation-value

$ kubectl get deploy test-nginx -n drift-verbose-7 -o jsonpath='{.metadata.annotations.custom/deploy-note}'
deploy-annotation-value

$ kubectl get svc test-service -n drift-verbose-7 -o jsonpath='{.metadata.annotations.custom/svc-note}'
svc-annotation-value

After reconciliation — annotations preserved on ALL resources, non-ignored drift corrected:

$ kubectl get cm test-config -n drift-verbose-7 -o jsonpath='{.metadata.annotations.custom/cm-note}'
cm-annotation-value  # <-- preserved

$ kubectl get deploy test-nginx -n drift-verbose-7 -o jsonpath='{.metadata.annotations.custom/deploy-note}'
deploy-annotation-value  # <-- preserved

$ kubectl get svc test-service -n drift-verbose-7 -o jsonpath='{.metadata.annotations.custom/svc-note}'
svc-annotation-value  # <-- preserved

$ kubectl get deploy test-nginx -n drift-verbose-7 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.25  # <-- corrected

$ kubectl get cm test-config -n drift-verbose-7 -o jsonpath='{.data.managed-key}'
v1  # <-- corrected

Controller log:

{
    "level": "info",
    "ts": "2026-05-12T16:43:18.546Z",
    "msg": "server-side apply completed",
    "controller": "kustomization",
    "controllerGroup": "kustomize.toolkit.fluxcd.io",
    "controllerKind": "Kustomization",
    "Kustomization": {
        "name": "test-no-target",
        "namespace": "drift-verbose-7"
    },
    "namespace": "drift-verbose-7",
    "name": "test-no-target",
    "reconcileID": "707b8268-1b35-4b53-a414-8ce081ca75a3",
    "output": {
        "ConfigMap/drift-verbose-7/test-config": "configured",
        "Deployment/drift-verbose-7/test-nginx": "configured",
        "Namespace/drift-verbose-7": "unchanged",
        "Service/drift-verbose-7/test-service": "unchanged"
    },
    "revision": "main@sha1:b2a96870abdf6a295501e298a147e1e4ea5d878a"
}

Test 8 — Only-ignored-field drift → Unchanged (no resource version bump)

Kustomization:

driftIgnoreRules:
- paths:
    - "/spec/replicas"
  target:
    kind: Deployment

Introduce drift ONLY in ignored field (spec.replicas via HPA SSA):

$ kubectl get deploy test-nginx -n drift-verbose-8 -o jsonpath='{.spec.replicas}'
5

$ kubectl get deploy test-nginx -n drift-verbose-8 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.25  # <-- no image drift

After reconciliation — Deployment treated as unchanged:

$ kubectl get deploy test-nginx -n drift-verbose-8 -o jsonpath='{.spec.replicas}'
5  # <-- replicas still at HPA value

Controller log — Deployment shows as "unchanged" (not "configured"):

{
    "level": "info",
    "ts": "2026-05-12T16:44:20.938Z",
    "msg": "server-side apply completed",
    "controller": "kustomization",
    "controllerGroup": "kustomize.toolkit.fluxcd.io",
    "controllerKind": "Kustomization",
    "Kustomization": {
        "name": "test-unchanged",
        "namespace": "drift-verbose-8"
    },
    "namespace": "drift-verbose-8",
    "name": "test-unchanged",
    "reconcileID": "e0c8033c-a6d7-49a1-901e-4a867a9cba69",
    "output": {
        "ConfigMap/drift-verbose-8/test-config": "unchanged",
        "Deployment/drift-verbose-8/test-nginx": "unchanged",
        "Namespace/drift-verbose-8": "unchanged",
        "Service/drift-verbose-8/test-service": "unchanged"
    },
    "revision": "main@sha1:b2a96870abdf6a295501e298a147e1e4ea5d878a"
}

Test 9 — Managed fields ownership — Flux releases ignored fields

Kustomization:

driftIgnoreRules:
- paths:
    - "/data/ignored-key"
  target:
    kind: ConfigMap
    name: test-config

After initial create, Flux owns ignored-key:

# Flux's Apply fieldsV1 contains "ignored-key":
"f:ignored-key":{}

After external controller claims ignored-key via SSA and reconciliation:

$ kubectl get cm test-config -n drift-verbose-9 -o jsonpath='{.data.ignored-key}'
external-value  # <-- external value preserved

$ kubectl get cm test-config -n drift-verbose-9 -o jsonpath='{.data.managed-key}'
v1  # <-- corrected to source

Flux no longer owns ignored-key (released ownership):

# Flux's Apply fieldsV1:
# Does NOT contain ignored-key — ownership released

# config-controller's Apply fieldsV1:
# Contains ignored-key — external controller owns it

Test 10 — Non-drifted ignored fields stay in payload (ownership retained)

Kustomization:

driftIgnoreRules:
- paths:
    - "/spec/replicas"
  target:
    kind: Deployment
    name: test-nginx

Drift ONLY non-ignored field (image), replicas NOT drifted:

$ kubectl get deploy test-nginx -n drift-verbose-10 -o jsonpath='{.spec.replicas}'
1  # <-- not drifted, stays in payload

$ kubectl get deploy test-nginx -n drift-verbose-10 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.25  # <-- corrected

Flux still owns spec.replicas (non-drifted, not stripped from payload):

# Flux fieldsV1 contains "replicas" — ownership retained

Test 11 — Label-selector targeting

Kustomization:

driftIgnoreRules:
- paths:
    - "/spec/replicas"
  target:
    kind: Deployment
    labelSelector: "team=platform"

Drift replicas + image, label selector team=platform matches:

$ kubectl get deploy test-nginx -n drift-verbose-11 -o jsonpath='{.spec.replicas}'
4  # <-- preserved (label selector matched)

$ kubectl get deploy test-nginx -n drift-verbose-11 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.25  # <-- corrected

Test 12 — driftAdopt with array-indexed path (merge key resolution)

Kustomization:

driftIgnoreRules:
- paths:
    - "/spec/template/spec/containers/0/resources"
  target:
    kind: Deployment
    name: test-nginx

Initial resources:

$ kubectl get deploy test-nginx -n drift-verbose-12 -o jsonpath='{.spec.template.spec.containers[0].resources}'
{"limits":{"cpu":"200m","memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}}

After drifting resources (non-SSA) and image, then reconciling:

$ kubectl get ks test-array-adopt -n drift-verbose-12 -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
True

$ kubectl get deploy test-nginx -n drift-verbose-12 -o jsonpath='{.spec.template.spec.containers[0].resources}'
{"limits":{"cpu":"1","memory":"1Gi"},"requests":{"cpu":"500m","memory":"512Mi"}}  # <-- driftAdopt preserved

$ kubectl get deploy test-nginx -n drift-verbose-12 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.25  # <-- corrected

Test 13 — Array index ownership — k:{name:nginx} resolves from index 0

Kustomization:

driftIgnoreRules:
- paths:
    - "/spec/template/spec/containers/0/resources"
  target:
    kind: Deployment
    name: test-nginx

VPA claims resources via SSA (separate Apply manager), then image tampered:

$ kubectl get deploy test-nginx -n drift-verbose-13 -o jsonpath='{.spec.template.spec.containers[0].resources.requests.cpu}'
300m  # <-- VPA value preserved (driftStrip)

$ kubectl get deploy test-nginx -n drift-verbose-13 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.25  # <-- corrected

vpa-recommender still owns resources in managedFields:

# vpa-recommender fieldsV1 contains "resources" under k:{"name":"nginx"}

Test 14 — kubectl edit/patch (Update-type) — driftAdopt across resources

Kustomization:

driftIgnoreRules:
- paths:
    - "/spec/replicas"
    - "/spec/template/spec/containers/0/resources"
  target:
    kind: Deployment
    name: test-nginx
- paths:
    - "/metadata/annotations/external-dns.alpha.kubernetes.io~1hostname"
  target:
    kind: Service
    name: test-service
- paths:
    - "/data/ignored-key"
  target:
    kind: ConfigMap
    name: test-config

Initial state:

$ kubectl get deploy test-nginx -n drift-verbose-15 -o jsonpath='{.spec.replicas}'
1

$ kubectl get deploy test-nginx -n drift-verbose-15 -o jsonpath='{.spec.template.spec.containers[0].resources}'
{"limits":{"cpu":"200m","memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}}

$ kubectl get svc test-service -n drift-verbose-15 -o jsonpath='{.metadata.annotations}'
{"external-dns.alpha.kubernetes.io/hostname":"test.example.com"}

$ kubectl get cm test-config -n drift-verbose-15 -o jsonpath='{.data}'
{"another-ignored":"keep-me","ignored-key":"original","managed-key":"v1"}

Simulate kubectl edit on all 3 resources (strategic merge / JSON / merge patches):

$ kubectl get deploy test-nginx -n drift-verbose-15 -o jsonpath='{.spec.replicas}'
5

$ kubectl get deploy test-nginx -n drift-verbose-15 -o jsonpath='{.spec.template.spec.containers[0].resources}'
{"limits":{"cpu":"1","memory":"1Gi"},"requests":{"cpu":"500m","memory":"512Mi"}}

$ kubectl get deploy test-nginx -n drift-verbose-15 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.24

$ kubectl get svc test-service -n drift-verbose-15 -o jsonpath='{.metadata.annotations.external-dns\.alpha\.kubernetes\.io/hostname}'
production.example.com

$ kubectl get svc test-service -n drift-verbose-15 -o jsonpath='{.spec.type}'
NodePort

$ kubectl get cm test-config -n drift-verbose-15 -o jsonpath='{.data}'
{"another-ignored":"keep-me","ignored-key":"edited-by-operator","managed-key":"tampered-by-kubectl"}

After reconciliation — driftAdopt across all resources:

$ kubectl get ks test-kubectl-edit -n drift-verbose-15 -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
True

# Deployment:
$ kubectl get deploy test-nginx -n drift-verbose-15 -o jsonpath='{.spec.replicas}'
5  # <-- preserved via driftAdopt

$ kubectl get deploy test-nginx -n drift-verbose-15 -o jsonpath='{.spec.template.spec.containers[0].resources}'
{"limits":{"cpu":"1","memory":"1Gi"},"requests":{"cpu":"500m","memory":"512Mi"}}  # <-- preserved via driftAdopt

$ kubectl get deploy test-nginx -n drift-verbose-15 -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.25  # <-- corrected

# Service:
$ kubectl get svc test-service -n drift-verbose-15 -o jsonpath='{.metadata.annotations.external-dns\.alpha\.kubernetes\.io/hostname}'
production.example.com  # <-- preserved via driftAdopt

$ kubectl get svc test-service -n drift-verbose-15 -o jsonpath='{.spec.type}'
ClusterIP  # <-- corrected

# ConfigMap:
$ kubectl get cm test-config -n drift-verbose-15 -o jsonpath='{.data}'
{"another-ignored":"keep-me","ignored-key":"edited-by-operator","managed-key":"v1"}
# ignored-key preserved via driftAdopt, managed-key corrected

Managed fields verification:

# Flux retains ownership of ignored fields (driftAdopt → not stripped):
# Flux owns replicas ✓
# Flux owns resources ✓

# No other Apply-type managers on Deployment (kubectl edit creates Update, not Apply):
# Other Apply managers: (none)
# Update managers: kube-controller-manager

Controller log:

{
    "level": "info",
    "ts": "2026-05-12T16:49:09.524Z",
    "msg": "server-side apply completed",
    "controller": "kustomization",
    "controllerGroup": "kustomize.toolkit.fluxcd.io",
    "controllerKind": "Kustomization",
    "Kustomization": {
        "name": "test-kubectl-edit",
        "namespace": "drift-verbose-15"
    },
    "namespace": "drift-verbose-15",
    "name": "test-kubectl-edit",
    "reconcileID": "f8770f04-a93c-42fa-83df-fdc934a7b6b3",
    "output": {
        "ConfigMap/drift-verbose-15/test-config": "configured",
        "Deployment/drift-verbose-15/test-nginx": "configured",
        "Namespace/drift-verbose-15": "unchanged",
        "Service/drift-verbose-15/test-service": "configured"
    },
    "revision": "main@sha1:b2a96870abdf6a295501e298a147e1e4ea5d878a"
}

@hiddeco
Copy link
Copy Markdown
Member

hiddeco commented Apr 20, 2026

Any specific reason for the API to not match the API of the helm-controller?

@dipti-pai dipti-pai force-pushed the drift-ignore-rules branch from 02e1e73 to e688fe8 Compare April 20, 2026 21:27
@stefanprodan
Copy link
Copy Markdown
Member

stefanprodan commented May 1, 2026

Any specific reason for the API to not match the API of the helm-controller?

We discussed this at the dev meetings. KC does drift detection and correction by design, so the envelop we use in HC would be confusing here, people would expect driftDetection.mode: disabled|warn while we can not support it. Even if this feature seems the same in KC and HC, it's actually very different. HC does not take into consideration the ignore rules when the Helm SDK client does the apply, a release upgrade blindly applies all changes even if there are ignore rules for certain fields. KC fully implements the rules, they are enforced not only during drift detection but also at apply time, given this, I think the API construct not matching HC is is a good thing.

Comment thread docs/spec/v1/kustomizations.md Outdated
@dipti-pai dipti-pai force-pushed the drift-ignore-rules branch from e688fe8 to d0de310 Compare May 8, 2026 23:09
Signed-off-by: Dipti Pai <diptipai89@outlook.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Constant rewriting of the CRD

3 participants