Skip to content

Internals

Read from the source at commit 989e001. Every claim here points at a file and line.

Code map

PathResponsibility
cmd/kyvernoAdmission controller entry point; wires the CEL engines and clients (cmd/kyverno/main.go).
pkg/webhooksWebhook HTTP server and route registration (pkg/webhooks/server.go).
pkg/webhooks/resourceAdmission handlers that categorize policies and call the engine (pkg/webhooks/resource/handlers.go).
pkg/engineThe policy evaluator behind the Engine interface (pkg/engine/engine.go).
pkg/engine/apiThe shared types: Engine, EngineResponse, RuleResponse.
pkg/celCEL-based policy engines (vpol, mpol, ivpol, gpol).
pkg/autogenRule auto-generation for Pod controllers; v1 and v2 implementations.
api/kyverno/v1Go types for ClusterPolicy / Policy, including Rule and Spec.
pkg/controllersReconcile loops: policy cache, status, cert manager, webhook registration.
pkg/backgroundAsync generate and mutate-existing via the UpdateRequest CRD.

Core data structures

Rule (api/kyverno/v1/rule_types.go:45) is the unit a policy author writes. One rule carries exactly one action: Mutation, Validation, Generation, or VerifyImages. It also holds Context for variables and data sources (rule_types.go:51), MatchResources (rule_types.go:61) and ExcludeResources (rule_types.go:67) for applicability, and two flavours of preconditions: the JMESPath RawAnyAllConditions (rule_types.go:82) and the CEL CELPreconditions (rule_types.go:87). The engine branches on predicate methods like HasValidate, HasValidatePodSecurity, and HasValidateCEL, so the rule type drives which handler runs.

Spec (api/kyverno/v1/spec_types.go:51) is the body of a ClusterPolicy or Policy. It exposes GetApplyRules() (spec_types.go:307), which can stop at the first matching rule, and GetFailurePolicy() (spec_types.go:277), which returns Fail or Ignore.

EngineResponse (pkg/engine/api/engineresponse.go:15) is the result for one policy: the original Resource, the PatchedResource after mutation, a PolicyResponse, and stats. It is created by NewEngineResponseFromPolicyContext (engineresponse.go:38).

RuleResponse (pkg/engine/api/ruleresponse.go:25) is the result for one rule: status, ruleType, message, generatedResources, patchedTarget, podSecurityChecks, exceptions, the native vapBinding (ruleresponse.go:48) and mapBinding (ruleresponse.go:50), and emitWarning (ruleresponse.go:52) which routes a rule message to the API server warning header.

A path worth tracing

Follow engine.validate for one resource. The loop over rules is the heart of evaluation:

go
// pkg/engine/validation.go:26
policyContext.JSONContext().Checkpoint()
defer policyContext.JSONContext().Restore()

gvk, _ := policyContext.ResourceKind()
for _, rule := range autogen.Default.ComputeRules(policy, gvk.Kind) {
    // handlerFactory picks a handler from the rule type
    // ...
    resource, ruleResp := e.invokeRuleHandler(ctx, logger, handlerFactory, ...)
}

Two things matter here. First, the rules iterated are not always the rules the author wrote: autogen.Default.ComputeRules (validation.go:30) can expand a Pod rule into rules for Deployment, DaemonSet, and the rest before the loop ever runs. Second, the handlerFactory (validation.go:33) resolves the handler lazily from predicates: NewValidateAssertHandler, NewValidateManifestHandler, NewValidatePssHandler, NewValidateCELHandler, or the default NewValidateResourceHandler (validation.go:58). The actual check runs in e.invokeRuleHandler (validation.go:72).

Things that surprised me

Autogen is more than a convenience. pkg/autogen/v1/autogen.go:207 ComputeRules derives pod-controller rules at evaluation time. CanAutoGen(spec) decides the candidate set of controllers (autogen.go:213), and the annotation pod-policies.kyverno.io/autogen-controllers can override which controllers are actually targeted (autogen.go:220). So a rule written for Pods quietly governs Deployments and CronJobs without the author writing those cases. There are two implementations: v1 and v2, and v2's ComputeRules returns an extra ExtractPodFunc (pkg/autogen/v2/autogen.go:303), a different signature for the same idea.

The audit path is intentionally fire-and-forget. After the deny decision is made, handlers.go:159 submits report creation to a pool with a fresh context.WithTimeout(context.Background(), 30*time.Second). The comment is explicit that this is independent of the HTTP request lifecycle, so a slow or failing report write cannot block or fail the admission response.

An empty engine response is a deliberate signal, not an error. In HandleValidationEnforce, engineResponse.IsNil() short-circuits the loop with a comment that old and new resources produced the same response, so an update that does not change the policy evaluation is allowed through (validation.go:108).