Dosu LogoDosu Logo
Ask
Join our Discord
kyvernoPublic
Nirmata
Documentskyverno
Background Scan Report Reconciliation
Background Scan Report Reconciliation
Type
Topic
Status
Published
Created
Aug 1, 2026
Updated
Aug 1, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

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:

  1. Resolve UID → resource: Look up the resource and its hash from the metadataCache. If the resource no longer exists, delete any associated ephemeral report .
  2. Fetch all policies: Load Kyverno policies, ValidatingPolicies, MutatingPolicies, ImageValidatingPolicies, VAPs, MAPs, and all their bindings and exceptions .
  3. Check needsReconcile: Determine whether scanning is needed and whether it should be full or partial .
  4. Update metadata cache if the observed hash differs from the stored hash — this is where the name/UID bug was introduced .
  5. Call reconcileReport if 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-time annotation is absent or older than forceDelay

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:

  1. Load existing report: Calls getReport(ctx, namespace, name) (where name = UID string) to fetch the existing EphemeralReport from the API server .
  2. Create skeleton if not found: If the report does not exist (IsNotFound), creates a new report object via NewBackgroundScanReport. This object has no .Name set — only .GenerateName = uid + "-"; the actual name is assigned by the API server on first create .
  3. Partial mode: In a partial reconcile, existing results for unchanged policies are carried forward from the observed report . Only policies/exceptions with changed resource versions are re-evaluated.
  4. Full mode: All policies are re-scanned with scanner.ScanResource .
  5. Store result: Delegates to storeReport.

storeReport: Create, Update, or Delete#

storeReport applies a simple state machine:

observed existsdesired has resultsAction
NoNoNo-op
NoYesCreate via CreateEphemeralReport
YesNoDelete the report
YesYesUpdate 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#

FilePurpose
pkg/controllers/report/background/controller.goMain controller: reconcile, needsReconcile, reconcileReport, storeReport
pkg/utils/report/new.goNewBackgroundScanReport — constructs report skeleton with GenerateName
pkg/controllers/report/resource/controller.goMetadataCache, UpdateResourceHash
PR #14755Fix: use r.Name not name in metadata cache update
PR #14787Cherry-pick of #14755 to release branch
Documents
Background Controller Trigger Validation
Background Controller UpdateRequest Processing
Background Mutation Engine
Background Scan Report Reconciliation
CEL Context Injection
CEL Policy Exception Handling
CLI Policy Result Processing
CLI Policy Testing
CLI Resource Resolution
CLI Worker Pool Management
Concurrency Safety
Engine Context Propagation
Generate Policy UpdateRequest Lifecycle
GeneratingPolicy Downstream Cleanup
GeneratingPolicy Synchronization and Reconciliation
Image Verification CEL Path
ImageValidatingPolicy Webhook Architecture
JMESPath Type Safety
JSON Patch Mutation
MutatingPolicy Resource Targeting
NamespaceSelector Policy Enforcement
OCI Referrers API Fallback
Policy Controller Reconciliation
Projected Service Account Token
Prometheus Metrics Integration
Report Controller Goroutine Lifecycle
Sigstore & TUF Integration
TTL Controller Lifecycle
ValidatingPolicy Autogen
ValidatingPolicy Engine
ValidatingPolicy Status Management
Variable Substitution
Webhook Generation
Webhook Lifecycle Management
Webhook Selector Grouping