Problem
When a DELETE external input fires, the MARK_FOR_DELETION effect replaces the entire object in state with the bare object from the scenario JSON rather than setting deletionTimestamp on the existing object in state.
This erases any fields that controllers have added since the initial environment — most critically, finalizers.
Root cause
In pkg/tracecheck/user_controller.go, the DELETE user action calls r.client.Delete(ctx, obj) where obj is constructed from the scenario JSON (no finalizer, no resourceRef, etc.). Delete() in pkg/replay/client.go:258-265 sets deletionTimestamp on this bare object and records it as a MARK_FOR_DELETION effect. Then applyEffects in pkg/tracecheck/explore.go:1838 replaces the existing state entry with this bare object.
Impact
In real Kubernetes, a DELETE request sets deletionTimestamp on the existing object, preserving all fields including finalizers. The object is only removed once all finalizers are cleared. In kamera, the DELETE replaces the object with a version that has no finalizers, so FinalizerReconciler immediately removes it — skipping the entire finalizer-gated cleanup lifecycle.
This caused a false positive in the Crossplane claim deletion scenario (C2 in examples/crossplane/.agents/ANALYSIS.md), where the claim's finalizer was erased by the DELETE, orphaning the XR. See crossplane/crossplane#7224.
Fix
The MARK_FOR_DELETION case in applyEffects should:
- Resolve the existing object from state
- Set
deletionTimestamp on a deep copy of the existing object
- Publish and store that merged version
This preserves finalizers, labels, annotations, and any other fields controllers have added.
Problem
When a DELETE external input fires, the
MARK_FOR_DELETIONeffect replaces the entire object in state with the bare object from the scenario JSON rather than settingdeletionTimestampon the existing object in state.This erases any fields that controllers have added since the initial environment — most critically, finalizers.
Root cause
In
pkg/tracecheck/user_controller.go, the DELETE user action callsr.client.Delete(ctx, obj)whereobjis constructed from the scenario JSON (no finalizer, no resourceRef, etc.).Delete()inpkg/replay/client.go:258-265setsdeletionTimestampon this bare object and records it as aMARK_FOR_DELETIONeffect. ThenapplyEffectsinpkg/tracecheck/explore.go:1838replaces the existing state entry with this bare object.Impact
In real Kubernetes, a DELETE request sets
deletionTimestampon the existing object, preserving all fields including finalizers. The object is only removed once all finalizers are cleared. In kamera, the DELETE replaces the object with a version that has no finalizers, soFinalizerReconcilerimmediately removes it — skipping the entire finalizer-gated cleanup lifecycle.This caused a false positive in the Crossplane claim deletion scenario (C2 in
examples/crossplane/.agents/ANALYSIS.md), where the claim's finalizer was erased by the DELETE, orphaning the XR. See crossplane/crossplane#7224.Fix
The
MARK_FOR_DELETIONcase inapplyEffectsshould:deletionTimestampon a deep copy of the existing objectThis preserves finalizers, labels, annotations, and any other fields controllers have added.