GeneratingPolicy Downstream Cleanup#
GeneratingPolicy (the CEL-based successor to ClusterPolicy generate rules) has two known gaps vs. legacy ClusterPolicy when it comes to cleaning up downstream resources:
- Webhook-level match condition evaluation: When a trigger UPDATE causes it to stop satisfying a
GeneratingPolicy'smatchConditions, no downstream cleanup UR is created β orphaning the previously generated resource.ClusterPolicyhandles this viasyncTriggerAction. - Background controller
SyncWatchersscoping:SyncWatcherstracks generated resources at the policy+GVR level rather than per-trigger UID, causing cross-trigger data loss and per-resource orphaning.
Status (as of 2026-09-03):
- Bug #16832 has been closed by PR #16920 (merged 2026-09-03). The fix propagates the
synchronizeoption toUpdateRequestsand changes policy update handling fromDeleteDownstreamstoInvalidateDownstreams. - Bug #16759 remains open.
Gap 1: No Webhook-Level Cleanup When Trigger Stops Matching#
ClusterPolicy behavior (working)#
In generation/handler.go, handleTrigger calls engine.ApplyBackgroundChecks on every admission event . Rules that fail (i.e., the resource no longer matches) are passed to syncTriggerAction. For each failed rule where rule.Generation.Synchronize == true, syncTriggerAction builds a UR with DeleteDownstream: true, queuing background deletion of the downstream.
GeneratingPolicy behavior (broken)#
gpol/handler.go's Generate runs entirely in a fire-and-forget goroutine . It does not call engine.ApplyBackgroundChecks or any equivalent CEL evaluation. Match filtering is performed upstream by the webhook routing layer β matchConstraints/matchConditions are applied on the ValidatingWebhookConfiguration and only already-matched policies are forwarded to this handler. The handler therefore never receives a "no longer matches" signal.
Consequence (issue #16832): When a namespace label is removed (or any trigger UPDATE causes matchConditions to evaluate to false), the gpol handler receives no request for that policy+trigger pair, creates no cleanup UR, and the downstream is orphaned indefinitely . The orphaned resource retains its ownership labels (generate.kyverno.io/policy-name, generate.kyverno.io/trigger-uid, etc.) but is never reclaimed.
No workaround via empty desired set: Returning [] from the CEL generate expression when the opt-in condition is absent also fails β the background controller does not delete existing downstreams when the desired set goes empty .
DeleteDownstream and Synchronize flags#
These flags are set in RuleContext by the webhook handler and consumed by ProcessUR in the background controller:
| Flag | Set by | Read by |
|---|---|---|
DeleteDownstream: true | gpol/handler.go on trigger DELETE when sync enabled | CELGenerateController.ProcessUR: calls DeleteDownstreams, skips generation |
Synchronize: true | gpol/handler.go on trigger UPDATE when sync enabled | CELGenerateController.ProcessUR: calls DeleteDownstreams before re-running generation |
For trigger DELETE the handler works correctly: if the policy does not include DELETE in its ResourceRules, deleteDownstream is set to true and (when sync is enabled) the UR is created .
For trigger UPDATE that stops matching: no UR is created at all. This is the missing path.
Legacy GenerateController comparison#
GenerateController.applyGenerate checks ruleContext.DeleteDownstream or a nil policy first and calls deleteDownstream. That function uses ur.Status.GeneratedResources if available, otherwise falls through to handleNonPolicyChanges which performs a label-based lookup via getDownstreams/fetch. The label query filters on generate.kyverno.io/trigger-uid with a name-based fallback for older resources .
Gap 2: SyncWatchers Per-Trigger Scoping Bug#
WatchManager.SyncWatchers is called in a goroutine after a successful generation pass when synchronize: true . The watcher tracks generated resources at policy+GVR granularity, not per-trigger UID.
Bug 1 β Cross-trigger deletion (#16759, ): When Trigger A evaluates to 0 generated resources, SyncWatchers sees an empty list for that GVR, loops through its metadataCache matching GeneratePolicyLabel == policyName, and deletes all cached downstreams across all triggers for that policy β including Trigger B's downstream.
Bug 2 β Individual resource orphan : If a policy returns 2 ConfigMaps for a single trigger and later only 1, SyncWatchers sees the GVR is still present in newGVRs and skips the deletion loop entirely. The second ConfigMap is never removed.
Root cause: SyncWatchers does not scope its diff to generate.kyverno.io/trigger-uid. The fix requires the watcher's diff logic to use GenerateTriggerUIDLabel to scope additions/deletions to the trigger currently being evaluated.
Partial fix in PR #16920: When a policy is updated, downstream resources are now invalidated in the metadata cache (by setting Hash = "") instead of being deleted. The watcher's handleUpdate and handleDelete methods check for invalidated entries and skip restoration. This prevents stale downstream resources from being restored to a previous version when a policy update event is processed before SyncWatchers() completes. The synchronize option is now correctly propagated to UpdateRequests, ensuring the update/delete handling logic in generate_controller.go is invoked when appropriate.
Key Files#
| File | Purpose |
|---|---|
pkg/webhooks/resource/gpol/handler.go | CEL GeneratingPolicy webhook handler β sets DeleteDownstream/Synchronize in URs |
pkg/webhooks/resource/generation/handler.go | Legacy ClusterPolicy handler β syncTriggerAction has the missing behavior |
pkg/background/gpol/generate_controller.go | CELGenerateController.ProcessUR β processes DeleteDownstream/Synchronize flags |
pkg/background/generate/controller.go | Legacy GenerateController.applyGenerate / deleteDownstream |
pkg/background/generate/cleanup.go | deleteDownstream / getDownstreams / fetch β label-based cleanup for legacy path |
Open Issues#
| Issue | Summary | Status |
|---|---|---|
| #16832 | Trigger UPDATE that stops matching matchConditions leaves downstream orphaned | Closed (PR #16920) |
| #16759 | SyncWatchers cross-trigger data loss and per-resource orphan bug | Open (partially addressed by PR #16920) |
| #16556 | generateExisting creates URs for all GVK resources, ignoring matchConditions | Open |