Background Scan Report Reconciliation#
Overview#
The background scan controller (pkg/controllers/report/background/controller.go) continuously reconciles EphemeralReport and ClusterEphemeralReport resources for every tracked Kubernetes resource. Its workqueue key is namespace/uid (namespaced) or uid (cluster-scoped) β the name segment always represents the resource UID, not its Kubernetes name .
Reconciliation Entry Point#
The outer reconcile loop (controller.reconcile) performs the following steps:
- Resolve UID β resource: Look up the resource and its hash from the
metadataCache. If the resource no longer exists, delete any associated ephemeral report . - Fetch all policies: Load Kyverno policies, ValidatingPolicies, MutatingPolicies, ImageValidatingPolicies, VAPs, MAPs, and all their bindings and exceptions .
- Check
needsReconcile: Determine whether scanning is needed and whether it should be full or partial . - Update metadata cache if the observed hash differs from the stored hash β this is where the name/UID bug was introduced .
- Call
reconcileReportif reconciliation is needed .
needsReconcile: Deciding Whether to Scan#
needsReconcile returns (observedHash, needsReconcile bool, full bool, error). A full reconcile is triggered when:
- No report metadata exists for this UID (first-time creation)
- The resource hash has changed (the underlying K8s resource was modified)
- The
audit.kyverno.io/last-scan-timeannotation is absent or older thanforceDelay
A partial reconcile is triggered when only policy or exception resource versions have changed (labels on the report differ from what is currently expected), without a resource hash change .
No reconcile (needsReconcile = false) is returned when the resource hash, scan time, and all policy/exception labels are all current .
reconcileReport: Building and Storing the Report#
reconcileReport handles both create and update paths:
- Load existing report: Calls
getReport(ctx, namespace, name)(wherename= UID string) to fetch the existingEphemeralReportfrom the API server . - Create skeleton if not found: If the report does not exist (
IsNotFound), creates a new report object viaNewBackgroundScanReport. This object has no.Nameset β only.GenerateName = uid + "-"; the actual name is assigned by the API server on first create . - Partial mode: In a partial reconcile, existing results for unchanged policies are carried forward from the
observedreport . Only policies/exceptions with changed resource versions are re-evaluated. - Full mode: All policies are re-scanned with
scanner.ScanResource. - Store result: Delegates to
storeReport.
storeReport: Create, Update, or Delete#
storeReport applies a simple state machine:
observed exists | desired has results | Action |
|---|---|---|
| No | No | No-op |
| No | Yes | Create via CreateEphemeralReport |
| Yes | No | Delete the report |
| Yes | Yes | Update via UpdateReport (if not identical) |
The name/generateName Mismatch#
Design Note: UID as Report Name#
Background scan reports use the resource UID as the lookup key. getReport fetches by (namespace, uid-string) , and the lister in getMeta does the same . When NewBackgroundScanReport constructs a new report object, it sets GenerateName = uid + "-" rather than Name . The actual API-server-assigned name (e.g. {uid}-x7q9z) differs from the lookup key. This works correctly in practice because the lister is backed by the informer cache which indexes by the real name, while getReport via the live API client uses the UID string β consistent across both paths because the report name is stored in labels/owner refs rather than relied upon directly for lookup identity.
The Metadata Cache name Corruption Bug (Fixed in #14755)#
A separate but related bug: when needsReconcile determined the observed hash differed from r.Hash, the controller called:
c.metadataCache.UpdateResourceHash(gvr, uid, resource.Resource{Name: name, ...})
Here name is parsed from the workqueue key (i.e., the UID), not the actual Kubernetes resource name . This corrupted the cache entry, causing subsequent GetResource calls to use the UID as the resource name, resulting in consistent NotFound errors and silent scan failures.
Fix (PR #14755, cherry-picked to PR #14787): Replace Name: name with Name: r.Name, where r is the Resource struct already stored in the cache with the correct Kubernetes name .
Key Files and References#
| File | Purpose |
|---|---|
pkg/controllers/report/background/controller.go | Main controller: reconcile, needsReconcile, reconcileReport, storeReport |
pkg/utils/report/new.go | NewBackgroundScanReport β constructs report skeleton with GenerateName |
pkg/controllers/report/resource/controller.go | MetadataCache, UpdateResourceHash |
| PR #14755 | Fix: use r.Name not name in metadata cache update |
| PR #14787 | Cherry-pick of #14755 to release branch |