GeneratingPolicy Synchronization and Reconciliation#
GeneratingPolicy (the CEL-based successor to ClusterPolicy generate rules) supports spec.evaluation.synchronize.enabled: true, which keeps downstream (generated) resources alive and in sync with their sources after initial creation. The background controller's CELGenerateController owns this behavior via two complementary mechanisms: the WatchManager for real-time event-driven reconciliation, and Server-Side Apply (SSA) for in-place drift correction.
Key entry points:
| File | Purpose |
|---|---|
pkg/background/gpol/generate_controller.go | CELGenerateController.ProcessUR β main UR processing loop |
pkg/background/gpol/dynamic_watcher.go | WatchManager β dynamic watches, create-on-delete, source-change propagation |
pkg/cel/libs/context.go | GenerateResources β resource creation/SSA reconciliation |
The Delete-and-Recreate Pattern (WatchManager)#
For synchronize: true policies, ProcessUR calls WatchManager.SyncWatchers (in a goroutine) after a successful generation pass, registering the generated resources' GVRs for ongoing watching . The WatchManager maintains:
dynamicWatchersβ map of GVR β livewatch.Interface+metadataCache(UID βResource)policyRefsβ map of policy name β tracked GVRsrefCountβ number of policies sharing each GVR watcher
On a delete event for a managed downstream resource (app.kubernetes.io/managed-by: kyverno), handleDelete looks up the resource by UID in the metadata cache. If the cached entry has a non-empty hash, it re-creates the resource via CreateResource. If the hash is empty (entry was invalidated), the resource is not recreated and the cache entry is removed. If the deleted object is a source resource (not managed by Kyverno), it instead deletes all downstreams carrying that source's UID label.
On an update event, handleUpdate retrieves the cached hash. If the cached hash is non-empty and differs from the incoming object's hash, the resource was externally modified, and the controller reverts it via UpdateResource. If the cached hash is empty (indicating the entry was invalidated), the revert logic is skipped and the new object is processed.
When the policy's generated GVRs change (e.g., the policy template is updated), SyncWatchers stops watching stale GVRs and deletes their associated downstream resources .
InvalidateDownstreamspre-step: Before a sync pass,ProcessURcallsWatchManager.InvalidateDownstreamswhenSynchronizeis set on the UR rule context. This invalidates downstream cache entries by clearing their hash (settingHash = ""), but does not delete the actual downstream resources. Invalidated resources are not restored after update or delete events, allowing the new generation result to be authoritative without prematurely removing existing resources .
SSA-Based In-Place Reconciliation#
PR #16403 (merged 2026-07-02) added spec.useServerSideApply support for GeneratingPolicy. When enabled, GenerateResources in pkg/cel/libs/context.go changes behavior:
- Resource does not exist: calls
ApplyResource(SSA, field manager"generate") instead ofCreateResource. - Resource exists and is policy-managed: re-applies via
ApplyResourceto reconcile drift without deleting and re-creating . - Resource exists but is not managed by this policy: skipped entirely β prevents silent adoption of user-created resources .
Ownership is checked by isManagedByPolicy, which requires all three of: app.kubernetes.io/managed-by: kyverno, generate.kyverno.io/policy-name: <policy>, and generate.kyverno.io/trigger-uid: <uid>.
Without SSA, the default behavior falls back to CreateResource for new resources and appends the existing resource to generatedResources unchanged . This means drift in non-SSA mode is not corrected by GenerateResources itself β it is corrected by the WatchManager's handleUpdate hash-comparison path.
SetGenerateContext wires the useServerSideApply flag from the policy spec into each worker's isolated context clone .
Historical Bugs and Fixes (v1.17.x)#
Several interconnected bugs in v1.17.x caused synchronize: true + generateExisting: true to produce an infinite DELETE β CREATE loop at ~15 ops/second . Four root causes were identified and fixed:
1. Shared CEL context race (PR #16041)#
Workers shared a global contextProvider singleton. Concurrent UpdateRequests for the same policy overwrote each other's genCtx state, causing a cache-restore pass to see zero generated resources and immediately deleting them. Fix: contextProvider.Clone() isolates each worker's state. The gate (!cacheRestore || len(resourcesToSync) > 0) prevents SyncWatchers from being called with an empty list .
2. Dead watcher goroutine entry (PR #16130)#
On large clusters (~50+ namespaces), the watcher goroutine would exit without clearing its entry from dynamicWatchers. Subsequent SyncWatchers calls saw the non-nil (dead) entry and skipped creating a replacement, permanently losing sync. Fix: The goroutine defers a cleanup using an identity check (dynamicWatchers[gvr] == thisWatcher) before nulling the watcher pointer; restart logic was added for nil-watcher entries .
3. Cache mutation via direct pointer (PR #16171)#
handleUpdate and handleDelete called SetUID(""), SetResourceVersion(""), etc., directly on the cached Data pointer, corrupting the cache. Hash comparisons on subsequent events failed against the corrupted state, triggering spurious reverts in a loop. Fix: All cache reads now use .DeepCopy() before mutation .
4. Pre-existing resource misclassification (PR #16280)#
When a generated resource already existed (e.g., after controller restart), GenerateResources treated it as "nothing generated" and omitted it from generatedResources, causing SyncWatchers to drop the watcher and then delete the resource. Fix: existing policy-managed resources are now appended to generatedResources; unmanaged resources are explicitly skipped to prevent adoption .
All four fixes are confirmed resolved on main .
Downstream-only-recreated-once (issue #15717)#
A related bug : a deleted downstream was recreated once but not on subsequent deletions. Root cause was the dead watcher goroutine (PR #16130) β after the first recreate, the new resource had a different UID and the goroutine tracking it had already exited. Fixed by the same PR #16130 watcher-restart logic.
Update propagation not working (issue #15727)#
When a trigger resource (e.g., HTTPRoute) was updated, the downstream (e.g., DNSEndpoint) retained stale data . This was also a symptom of the dead watcher / cache mutation bugs above, resolved on main by the same PR chain.
Known Limitations and Operational Notes#
ownerReferencesstripping: When a cloned source resource is in a different namespace than the target, inheritedownerReferencesare stripped automatically to prevent the garbage collector from deleting the copy .- Cache invalidation, not upfront deletion:
ProcessURcallsInvalidateDownstreamsbefore the new generation pass wheneverSynchronizeis true on the UR rule context . This marks cache entries as invalid (by clearing their hash) rather than deleting downstream resources upfront. Downstream resources remain in the cluster and are updated in place during the generation pass; stale resources (those no longer part of the policy output) are removed by the reconciliation logic after the generation completes. - RBAC for watched resources: The background controller ServiceAccount must have
watchon the generated resource kind. Missing RBAC causes the watcher to fail silently, breaking all recreate/revert behavior. operations: [CREATE, UPDATE]+generateExisting: A common workaround for v1.17.x was to use onlyoperations: [CREATE], which avoids the UPDATE path that was triggering spurious resyncs . This workaround is no longer necessary on currentmain.- Legacy
ClusterPolicycomparison: The oldClusterPolicysynchronize: truepath uses a different reconciler (pkg/background/generate/controller.go) and does not share theWatchManager. It relies onuseServerSideApplyat the spec level and label-based cleanup viagetDownstreams/fetchrather than in-memory cache.