CLI Resource Resolution#
The Kyverno CLI uses a composite resource key format to identify, fetch, match, and display resources during apply and test flows. This article covers the key-building logic, the two resource-fetching paths (cluster vs. local), how resource kinds are resolved to GVRs for CEL policies, and the known edge cases around hyphen-delimited identifiers and cluster-scoped resource formatting.
Key files:
| File | Responsibility |
|---|---|
cmd/cli/kubectl-kyverno/utils/common/fetch.go | ResourceFetcher, cluster/local resource loading, GVK extraction per policy type |
cmd/cli/kubectl-kyverno/commands/test/test.go | generateResourceKey(), test orchestration |
cmd/cli/kubectl-kyverno/processor/policy_processor.go | resolveResource(), GVKβGVR fallback for non-cluster mode |
cmd/cli/kubectl-kyverno/commands/test/output.go | Display formatting, commaβslash conversion |
Composite Resource Key Format#
generateResourceKey() in test.go produces a comma-separated string:
apiVersion,kind,namespace,name
For cluster-scoped resources (empty namespace), this yields keys like v1,ClusterRole,,cluster-admin. For namespaced resources: apps/v1,Deployment,default,nginx.
These keys are used internally for deduplication and test result matching throughout the test command execution.
In the apply flow's listResources(), a hyphen-delimited variant is used instead :
key := fmt.Sprintf("%s-%s-%s", gvk.Kind, resource.GetNamespace(), resource.GetName())
This format is used as a map key in getFromCluster() . When filtering resources against ResourcePaths, the code splits on - : if the namespace or resource name contains hyphens (e.g., longhorn-system, my-custom-resource), the split breaks the lookup, causing a false "not found in cluster" error.
Resource Fetching (apply flow)#
ResourceFetcher.GetResources() routes to one of two paths:
- Cluster mode (
--clusterflag + client present): callsgetFromCluster(), which callsextractResourcesFromPolicies()to collect GVKs, then fetches matching resources vialistResources()or the concurrent loader. - Local mode: reads YAML/JSON files from
ResourcePathsviagetFromLocalFiles().
extractResourcesFromPolicies() dispatches per policy type to build the set of GVKs to fetch:
- Legacy Kyverno Policy (
AsKyvernoPolicy()): walks autogenerated rules viagetKindsFromRule() - CEL policies (ValidatingAdmissionPolicy, ValidatingPolicy, ImageValidatingPolicy, DeletingPolicy, MutatingAdmissionPolicy, GeneratingPolicy, NamespacedGeneratingPolicy, MutatingPolicy, NamespacedMutatingPolicy): extract
MatchConstraintsand callgetKindsFromPolicy()
Note: Branches for
MutatingPolicy,NamespacedMutatingPolicy, andNamespacedGeneratingPolicywere missing prior to PR #15784, causing those policy types to silently fetch no resources from the cluster.
getKindsFromPolicy() uses the REST mapper to expand wildcard kinds in MatchConstraints . addToresourceTypeInfo() calls Discovery().FindResources() to resolve each kind to its GVK and populates either gvkMap (resources) or subresourceMap (subresources) .
GVK β GVR Resolution (test flow, non-cluster mode)#
In non-cluster test mode, the PolicyProcessor has no REST mapper available. resolveResource(kind) bridges this gap with a two-stage approach:
- Primary:
meta.UnsafeGuessKindToResource()β works for standard Kubernetes kinds by lowercasing and pluralizing. - Fallback: Scans
MatchConstraints.ResourceRulesinValidatingPolicies, thenMutatingPolicies, thenGeneratingPolicies, looking for an exact match (strings.ToLower(base) == kindLower) against the listed resource names . This is the path taken for CRDs whose plural forms can't be automatically inferred.
resolveResource is called when restMapper.RESTMapping() fails for:
- MutatingPolicy
- ValidatingPolicy
- GeneratingPolicy
Changes introduced by PR #15784:
- Before: only
ValidatingPolicieswere scanned in the fallback β MutatingPolicy and GeneratingPolicy would error on CRDs. - After: all three policy types are scanned.
- Matching changed from
strings.HasPrefix()to exact equality (strings.ToLower(base) == kindLower) to prevent false positives from prefix collisions. - Added
tryClusterOpenAPI()with panic recovery to handle the fake discovery client used in test mode, which doesn't implementOpenAPIV3().
ClusterResources and isFake Bugs#
When a kyverno test test case defines clusterResources, those resources must be loaded into the fake client so that context lookups (e.g., resource.Get(...)) succeed during evaluation. The isFake boolean parameter controls whether the fake client is populated from clusterResources.
- ImageValidatingPolicy (PR #15418): Had a hardcoded
trueforisFake, causing allclusterResourcesto be ignored. Fixed to!(len(testCase.Test.ClusterResources) > 0). - DeletingPolicy (PR #15784): Same hardcoded
trueissue, fixed with the same pattern.
Additionally, the DeletingPolicy CEL compiler was missing common.ResolverEnvOption(&mutation.DynamicTypeResolver{}) in its base options (fixed in pkg/cel/policies/dpol/compiler/compiler.go in PR #15784), which caused panics when evaluating expressions like object.metadata.namespace against resources β manifesting as "no matches for kind" errors when targeting CRDs via kyverno apply (see issue #15292).
Output Formatting: Double-Slash Bug#
output.go formats resource keys for display by replacing all commas with slashes:
strings.Replace(resource, ",", "/", -1)
For cluster-scoped resources, generateResourceKey() produces v1,ClusterRole,,cluster-admin (empty namespace field). After the replacement, this becomes v1/ClusterRole//cluster-admin β an extra slash from the empty namespace .
The correct expected output is:
- Cluster-scoped:
v1/ClusterRole/cluster-admin - Namespaced:
apps/v1/Deployment/default/nginx
Fix (PR #16627, filed against issue #16626): A new formatResource(resourceKey string) string helper parses the four-part comma-separated key and omits the namespace segment when empty, replacing all ad-hoc strings.Replace(resource, ",", "/", -1) calls across output.go.
Status as of July 2026: PR #16627 was open/pending review. Check the PR for merge status before relying on this behavior being fixed in your installed version.