diff --git a/cmd/cli/kubectl-kyverno/utils/common/common.go b/cmd/cli/kubectl-kyverno/utils/common/common.go index 573ed7fe59..79a45c30d8 100644 --- a/cmd/cli/kubectl-kyverno/utils/common/common.go +++ b/cmd/cli/kubectl-kyverno/utils/common/common.go @@ -461,10 +461,7 @@ OuterLoop: log.Log.Error(err, "failed to add image variables to context") } - subresources := make([]struct { - APIResource metav1.APIResource - ParentResource metav1.APIResource - }, 0) + subresources := make([]engineapi.SubResource, 0) // If --cluster flag is not set, then we need to add subresources to the context if c.Client == nil { diff --git a/pkg/engine/api/policycontext.go b/pkg/engine/api/policycontext.go new file mode 100644 index 0000000000..93c42458a1 --- /dev/null +++ b/pkg/engine/api/policycontext.go @@ -0,0 +1,45 @@ +package api + +import ( + "context" + + kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" + kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1" + kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1" + "github.com/kyverno/kyverno/pkg/clients/dclient" + enginecontext "github.com/kyverno/kyverno/pkg/engine/context" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// ExcludeFunc is a function used to determine if a resource is excluded +type ExcludeFunc = func(kind, namespace, name string) bool + +type SubResource struct { + APIResource metav1.APIResource + ParentResource metav1.APIResource +} + +type PolicyContext interface { + Policy() kyvernov1.PolicyInterface + NewResource() unstructured.Unstructured + OldResource() unstructured.Unstructured + AdmissionInfo() kyvernov1beta1.RequestInfo + NamespaceLabels() map[string]string + SubResource() string + SubresourcesInPolicy() []SubResource + ExcludeGroupRole() []string + AdmissionOperation() bool + RequestResource() metav1.GroupVersionResource + Element() unstructured.Unstructured + SetElement(element unstructured.Unstructured) + + JSONContext() enginecontext.Interface + Client() dclient.Interface + Copy() PolicyContext + + FindExceptions(rule string) ([]*kyvernov2alpha1.PolicyException, error) + ExcludeResourceFunc() ExcludeFunc + ResolveConfigMap(ctx context.Context, namespace string, name string) (*corev1.ConfigMap, error) +} diff --git a/pkg/engine/background.go b/pkg/engine/background.go index ad8e5cc12e..2cffa409db 100644 --- a/pkg/engine/background.go +++ b/pkg/engine/background.go @@ -19,7 +19,7 @@ import ( // 2. returns the list of rules that are applicable on this policy and resource, if 1 succeed func ApplyBackgroundChecks( contextLoader ContextLoaderFactory, - policyContext *PolicyContext, + policyContext engineapi.PolicyContext, ) (resp *engineapi.EngineResponse) { policyStartTime := time.Now() return filterRules(contextLoader, policyContext, policyStartTime) @@ -27,18 +27,20 @@ func ApplyBackgroundChecks( func filterRules( contextLoader ContextLoaderFactory, - policyContext *PolicyContext, + policyContext engineapi.PolicyContext, startTime time.Time, ) *engineapi.EngineResponse { - kind := policyContext.newResource.GetKind() - name := policyContext.newResource.GetName() - namespace := policyContext.newResource.GetNamespace() - apiVersion := policyContext.newResource.GetAPIVersion() + newResource := policyContext.NewResource() + policy := policyContext.Policy() + kind := newResource.GetKind() + name := newResource.GetName() + namespace := newResource.GetNamespace() + apiVersion := newResource.GetAPIVersion() resp := &engineapi.EngineResponse{ PolicyResponse: engineapi.PolicyResponse{ Policy: engineapi.PolicySpec{ - Name: policyContext.policy.GetName(), - Namespace: policyContext.policy.GetNamespace(), + Name: policy.GetName(), + Namespace: policy.GetNamespace(), }, PolicyStats: engineapi.PolicyStats{ ExecutionStats: engineapi.ExecutionStats{ @@ -54,13 +56,13 @@ func filterRules( }, } - if policyContext.excludeResourceFunc(kind, namespace, name) { + if policyContext.ExcludeResourceFunc()(kind, namespace, name) { logging.WithName("ApplyBackgroundChecks").Info("resource excluded", "kind", kind, "namespace", namespace, "name", name) return resp } - applyRules := policyContext.policy.GetSpec().GetApplyRules() - for _, rule := range autogen.ComputeRules(policyContext.policy) { + applyRules := policy.GetSpec().GetApplyRules() + for _, rule := range autogen.ComputeRules(policy) { if ruleResp := filterRule(contextLoader, rule, policyContext); ruleResp != nil { resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp) if applyRules == kyvernov1.ApplyOne && ruleResp.Status != engineapi.RuleStatusSkip { @@ -75,7 +77,7 @@ func filterRules( func filterRule( contextLoader ContextLoaderFactory, rule kyvernov1.Rule, - policyContext *PolicyContext, + policyContext engineapi.PolicyContext, ) *engineapi.RuleResponse { if !rule.HasGenerate() && !rule.IsMutateExisting() { return nil @@ -99,21 +101,21 @@ func filterRule( startTime := time.Now() - policy := policyContext.policy - newResource := policyContext.newResource - oldResource := policyContext.oldResource - admissionInfo := policyContext.admissionInfo - ctx := policyContext.jsonContext - excludeGroupRole := policyContext.excludeGroupRole - namespaceLabels := policyContext.namespaceLabels + policy := policyContext.Policy() + newResource := policyContext.NewResource() + oldResource := policyContext.OldResource() + admissionInfo := policyContext.AdmissionInfo() + ctx := policyContext.JSONContext() + excludeGroupRole := policyContext.ExcludeGroupRole() + namespaceLabels := policyContext.NamespaceLabels() logger = logging.WithName(string(ruleType)).WithValues("policy", policy.GetName(), "kind", newResource.GetKind(), "namespace", newResource.GetNamespace(), "name", newResource.GetName()) - if err := MatchesResourceDescription(subresourceGVKToAPIResource, newResource, rule, admissionInfo, excludeGroupRole, namespaceLabels, "", policyContext.subresource); err != nil { + if err := MatchesResourceDescription(subresourceGVKToAPIResource, newResource, rule, admissionInfo, excludeGroupRole, namespaceLabels, "", policyContext.SubResource()); err != nil { if ruleType == engineapi.Generation { // if the oldResource matched, return "false" to delete GR for it - if err = MatchesResourceDescription(subresourceGVKToAPIResource, oldResource, rule, admissionInfo, excludeGroupRole, namespaceLabels, "", policyContext.subresource); err == nil { + if err = MatchesResourceDescription(subresourceGVKToAPIResource, oldResource, rule, admissionInfo, excludeGroupRole, namespaceLabels, "", policyContext.SubResource()); err == nil { return &engineapi.RuleResponse{ Name: rule.Name, Type: ruleType, @@ -129,8 +131,8 @@ func filterRule( return nil } - policyContext.jsonContext.Checkpoint() - defer policyContext.jsonContext.Restore() + policyContext.JSONContext().Checkpoint() + defer policyContext.JSONContext().Restore() if err := LoadContext(context.TODO(), contextLoader, rule.Context, policyContext, rule.Name); err != nil { logger.V(4).Info("cannot add external data to the context", "reason", err.Error()) diff --git a/pkg/engine/common.go b/pkg/engine/common.go index 4d43f6c0b8..32ad0bc771 100644 --- a/pkg/engine/common.go +++ b/pkg/engine/common.go @@ -3,20 +3,22 @@ package engine import ( "strings" + engineapi "github.com/kyverno/kyverno/pkg/engine/api" kubeutils "github.com/kyverno/kyverno/pkg/utils/kube" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // GetSubresourceGVKToAPIResourceMap returns a map of subresource GVK to APIResource. This is used to determine if a resource is a subresource. -func GetSubresourceGVKToAPIResourceMap(kindsInPolicy []string, ctx *PolicyContext) map[string]*metav1.APIResource { +func GetSubresourceGVKToAPIResourceMap(kindsInPolicy []string, ctx engineapi.PolicyContext) map[string]*metav1.APIResource { subresourceGVKToAPIResource := make(map[string]*metav1.APIResource) for _, gvk := range kindsInPolicy { gv, k := kubeutils.GetKindFromGVK(gvk) parentKind, subresource := kubeutils.SplitSubresource(k) // Len of subresources is non zero only when validation request was sent from CLI without connecting to the cluster. - if len(ctx.subresourcesInPolicy) != 0 { + subresourcesInPolicy := ctx.SubresourcesInPolicy() + if len(subresourcesInPolicy) != 0 { if subresource != "" { - for _, subresourceInPolicy := range ctx.subresourcesInPolicy { + for _, subresourceInPolicy := range subresourcesInPolicy { parentResourceGroupVersion := metav1.GroupVersion{ Group: subresourceInPolicy.ParentResource.Group, Version: subresourceInPolicy.ParentResource.Version, @@ -31,7 +33,7 @@ func GetSubresourceGVKToAPIResourceMap(kindsInPolicy []string, ctx *PolicyContex } } } else { // Complete kind may be a subresource, for eg- 'PodExecOptions' - for _, subresourceInPolicy := range ctx.subresourcesInPolicy { + for _, subresourceInPolicy := range subresourcesInPolicy { // Subresources which can be just specified by kind, for eg- 'PodExecOptions' // have different kind than their parent resource. Otherwise for subresources which // have same kind as parent resource, need to be specified as Kind/Subresource, eg - 'Pod/status' @@ -48,9 +50,9 @@ func GetSubresourceGVKToAPIResourceMap(kindsInPolicy []string, ctx *PolicyContex } } } - } else if ctx.client != nil { + } else if ctx.Client() != nil { // find the resource from API client - apiResource, _, _, err := ctx.client.Discovery().FindResource(gv, k) + apiResource, _, _, err := ctx.Client().Discovery().FindResource(gv, k) if err == nil { if kubeutils.IsSubresource(apiResource.Name) { subresourceGVKToAPIResource[gvk] = apiResource diff --git a/pkg/engine/common_test.go b/pkg/engine/common_test.go index 939a89a56f..2d5291a0ad 100644 --- a/pkg/engine/common_test.go +++ b/pkg/engine/common_test.go @@ -3,6 +3,7 @@ package engine import ( "testing" + engineapi "github.com/kyverno/kyverno/pkg/engine/api" "gotest.tools/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -37,10 +38,7 @@ func Test_GetSubresourceGVKToAPIResourceMap(t *testing.T) { } policyContext := NewPolicyContext(). - WithSubresourcesInPolicy([]struct { - APIResource metav1.APIResource - ParentResource metav1.APIResource - }{ + WithSubresourcesInPolicy([]engineapi.SubResource{ { APIResource: podStatusAPIResource, ParentResource: podAPIResource, diff --git a/pkg/engine/generation.go b/pkg/engine/generation.go index e60f7c2794..d42d786343 100644 --- a/pkg/engine/generation.go +++ b/pkg/engine/generation.go @@ -13,7 +13,7 @@ import ( // GenerateResponse checks for validity of generate rule on the resource func GenerateResponse( contextLoader ContextLoaderFactory, - policyContext *PolicyContext, + policyContext engineapi.PolicyContext, gr kyvernov1beta1.UpdateRequest, ) (resp *engineapi.EngineResponse) { policyStartTime := time.Now() @@ -22,19 +22,19 @@ func GenerateResponse( func filterGenerateRules( contextLoader ContextLoaderFactory, - policyContext *PolicyContext, + policyContext engineapi.PolicyContext, policyNameKey string, startTime time.Time, ) *engineapi.EngineResponse { - kind := policyContext.newResource.GetKind() - name := policyContext.newResource.GetName() - namespace := policyContext.newResource.GetNamespace() - apiVersion := policyContext.newResource.GetAPIVersion() + newResource := policyContext.NewResource() + kind := newResource.GetKind() + name := newResource.GetName() + namespace := newResource.GetNamespace() + apiVersion := newResource.GetAPIVersion() pNamespace, pName, err := cache.SplitMetaNamespaceKey(policyNameKey) if err != nil { logging.Error(err, "failed to spilt name and namespace", policyNameKey) } - resp := &engineapi.EngineResponse{ PolicyResponse: engineapi.PolicyResponse{ Policy: engineapi.PolicySpec{ @@ -54,13 +54,12 @@ func filterGenerateRules( }, }, } - - if policyContext.excludeResourceFunc(kind, namespace, name) { + if policyContext.ExcludeResourceFunc()(kind, namespace, name) { logging.WithName("Generate").Info("resource excluded", "kind", kind, "namespace", namespace, "name", name) return resp } - for _, rule := range autogen.ComputeRules(policyContext.policy) { + for _, rule := range autogen.ComputeRules(policyContext.Policy()) { if ruleResp := filterRule(contextLoader, rule, policyContext); ruleResp != nil { resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp) } diff --git a/pkg/engine/imageVerify.go b/pkg/engine/imageVerify.go index e612a107b1..b008a9a0bc 100644 --- a/pkg/engine/imageVerify.go +++ b/pkg/engine/imageVerify.go @@ -47,15 +47,15 @@ func getMatchingImages(images map[string]map[string]apiutils.ImageInfo, rule *ky return imageInfos, strings.Join(imageRefs, ",") } -func extractMatchingImages(policyContext *PolicyContext, rule *kyvernov1.Rule, cfg config.Configuration) ([]apiutils.ImageInfo, string, error) { +func extractMatchingImages(policyContext engineapi.PolicyContext, rule *kyvernov1.Rule, cfg config.Configuration) ([]apiutils.ImageInfo, string, error) { var ( images map[string]map[string]apiutils.ImageInfo err error ) - images = policyContext.jsonContext.ImageInfo() + newResource := policyContext.NewResource() + images = policyContext.JSONContext().ImageInfo() if rule.ImageExtractors != nil { - images, err = policyContext.jsonContext.GenerateCustomImageInfo( - &policyContext.newResource, rule.ImageExtractors, cfg) + images, err = policyContext.JSONContext().GenerateCustomImageInfo(&newResource, rule.ImageExtractors, cfg) if err != nil { // if we get an error while generating custom images from image extractors, // don't check for matching images in imageExtractors @@ -70,13 +70,13 @@ func VerifyAndPatchImages( ctx context.Context, contextLoader ContextLoaderFactory, rclient registryclient.Client, - policyContext *PolicyContext, + policyContext engineapi.PolicyContext, cfg config.Configuration, ) (*engineapi.EngineResponse, *ImageVerificationMetadata) { resp := &engineapi.EngineResponse{} - policy := policyContext.policy - patchedResource := policyContext.newResource + policy := policyContext.Policy() + patchedResource := policyContext.NewResource() logger := logging.WithName("EngineVerifyImages").WithValues("policy", policy.GetName(), "kind", patchedResource.GetKind(), "namespace", patchedResource.GetNamespace(), "name", patchedResource.GetName()) @@ -88,11 +88,11 @@ func VerifyAndPatchImages( "applied", resp.PolicyResponse.RulesAppliedCount, "successful", resp.IsSuccessful()) }() - policyContext.jsonContext.Checkpoint() - defer policyContext.jsonContext.Restore() + policyContext.JSONContext().Checkpoint() + defer policyContext.JSONContext().Restore() ivm := &ImageVerificationMetadata{} - rules := autogen.ComputeRules(policyContext.policy) + rules := autogen.ComputeRules(policyContext.Policy()) applyRules := policy.GetSpec().GetApplyRules() for i := range rules { @@ -138,13 +138,13 @@ func VerifyAndPatchImages( return } - policyContext.jsonContext.Restore() + policyContext.JSONContext().Restore() if err := LoadContext(ctx, contextLoader, rule.Context, policyContext, rule.Name); err != nil { appendResponse(resp, rule, fmt.Sprintf("failed to load context: %s", err.Error()), engineapi.RuleStatusError) return } - ruleCopy, err := substituteVariables(rule, policyContext.jsonContext, logger) + ruleCopy, err := substituteVariables(rule, policyContext.JSONContext(), logger) if err != nil { appendResponse(resp, rule, fmt.Sprintf("failed to substitute variables: %s", err.Error()), engineapi.RuleStatusError) return @@ -203,7 +203,7 @@ func substituteVariables(rule *kyvernov1.Rule, ctx enginecontext.EvalInterface, type imageVerifier struct { logger logr.Logger rclient registryclient.Client - policyContext *PolicyContext + policyContext engineapi.PolicyContext rule *kyvernov1.Rule resp *engineapi.EngineResponse ivm *ImageVerificationMetadata @@ -228,13 +228,13 @@ func (iv *imageVerifier) verify(ctx context.Context, imageVerify kyvernov1.Image } pointer := jsonpointer.ParsePath(imageInfo.Pointer).JMESPath() - changed, err := iv.policyContext.jsonContext.HasChanged(pointer) + changed, err := iv.policyContext.JSONContext().HasChanged(pointer) if err == nil && !changed { iv.logger.V(4).Info("no change in image, skipping check", "image", image) continue } - verified, err := isImageVerified(iv.policyContext.newResource, image, iv.logger) + verified, err := isImageVerified(iv.policyContext.NewResource(), image, iv.logger) if err == nil && verified { iv.logger.Info("image was previously verified, skipping check", "image", image) continue @@ -292,15 +292,17 @@ func (iv *imageVerifier) handleMutateDigest(ctx context.Context, digest string, return patch, digest, nil } -func hasImageVerifiedAnnotationChanged(ctx *PolicyContext, log logr.Logger) bool { - if reflect.DeepEqual(ctx.newResource, unstructured.Unstructured{}) || - reflect.DeepEqual(ctx.oldResource, unstructured.Unstructured{}) { +func hasImageVerifiedAnnotationChanged(ctx engineapi.PolicyContext, log logr.Logger) bool { + newResource := ctx.NewResource() + oldResource := ctx.OldResource() + if reflect.DeepEqual(newResource, unstructured.Unstructured{}) || + reflect.DeepEqual(oldResource, unstructured.Unstructured{}) { return false } key := imageVerifyAnnotationKey - newValue := ctx.newResource.GetAnnotations()[key] - oldValue := ctx.oldResource.GetAnnotations()[key] + newValue := newResource.GetAnnotations()[key] + oldValue := oldResource.GetAnnotations()[key] result := newValue != oldValue if result { log.V(2).Info("annotation mismatch", "oldValue", oldValue, "newValue", newValue, "key", key) @@ -333,7 +335,7 @@ func (iv *imageVerifier) verifyImage( iv.logger.V(2).Info("verifying image signatures", "image", image, "attestors", len(imageVerify.Attestors), "attestations", len(imageVerify.Attestations)) - if err := iv.policyContext.jsonContext.AddImageInfo(imageInfo, cfg); err != nil { + if err := iv.policyContext.JSONContext().AddImageInfo(imageInfo, cfg); err != nil { iv.logger.Error(err, "failed to add image to context") msg := fmt.Sprintf("failed to add image to context %s: %s", image, err.Error()) return ruleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusError), "" @@ -698,10 +700,10 @@ func (iv *imageVerifier) checkAttestations(a kyvernov1.Attestation, s map[string return true, nil } - iv.policyContext.jsonContext.Checkpoint() - defer iv.policyContext.jsonContext.Restore() + iv.policyContext.JSONContext().Checkpoint() + defer iv.policyContext.JSONContext().Restore() - return evaluateConditions(a.Conditions, iv.policyContext.jsonContext, s, iv.logger) + return evaluateConditions(a.Conditions, iv.policyContext.JSONContext(), s, iv.logger) } func evaluateConditions( diff --git a/pkg/engine/imageVerifyValidate.go b/pkg/engine/imageVerifyValidate.go index 181d8fa1af..b04763cd26 100644 --- a/pkg/engine/imageVerifyValidate.go +++ b/pkg/engine/imageVerifyValidate.go @@ -19,7 +19,7 @@ func processImageValidationRule( ctx context.Context, contextLoader ContextLoaderFactory, log logr.Logger, - enginectx *PolicyContext, + enginectx engineapi.PolicyContext, rule *kyvernov1.Rule, cfg config.Configuration, ) *engineapi.RuleResponse { @@ -51,7 +51,7 @@ func processImageValidationRule( } if !preconditionsPassed { - if enginectx.policy.GetSpec().ValidationFailureAction.Audit() { + if enginectx.Policy().GetSpec().ValidationFailureAction.Audit() { return nil } @@ -60,7 +60,7 @@ func processImageValidationRule( for _, v := range rule.VerifyImages { imageVerify := v.Convert() - for _, infoMap := range enginectx.jsonContext.ImageInfo() { + for _, infoMap := range enginectx.JSONContext().ImageInfo() { for name, imageInfo := range infoMap { image := imageInfo.String() log = log.WithValues("rule", rule.Name) @@ -82,24 +82,22 @@ func processImageValidationRule( return ruleResponse(*rule, engineapi.Validation, "image verified", engineapi.RuleStatusPass) } -func validateImage(ctx *PolicyContext, imageVerify *kyvernov1.ImageVerification, name string, imageInfo apiutils.ImageInfo, log logr.Logger) error { +func validateImage(ctx engineapi.PolicyContext, imageVerify *kyvernov1.ImageVerification, name string, imageInfo apiutils.ImageInfo, log logr.Logger) error { image := imageInfo.String() if imageVerify.VerifyDigest && imageInfo.Digest == "" { log.V(2).Info("missing digest", "image", imageInfo.String()) return fmt.Errorf("missing digest for %s", image) } - - if imageVerify.Required && !reflect.DeepEqual(ctx.newResource, unstructured.Unstructured{}) { - verified, err := isImageVerified(ctx.newResource, image, log) + newResource := ctx.NewResource() + if imageVerify.Required && !reflect.DeepEqual(newResource, unstructured.Unstructured{}) { + verified, err := isImageVerified(newResource, image, log) if err != nil { return err } - if !verified { return fmt.Errorf("unverified image %s", image) } } - return nil } diff --git a/pkg/engine/imageVerify_test.go b/pkg/engine/imageVerify_test.go index 9670a27a85..26462d5644 100644 --- a/pkg/engine/imageVerify_test.go +++ b/pkg/engine/imageVerify_test.go @@ -163,7 +163,7 @@ var cfg = config.NewDefaultConfiguration() func doVerifyAndPatchImages( ctx context.Context, rclient registryclient.Client, - pContext *PolicyContext, + pContext engineapi.PolicyContext, cfg config.Configuration, ) (*engineapi.EngineResponse, *ImageVerificationMetadata) { return VerifyAndPatchImages( diff --git a/pkg/engine/jsonContext.go b/pkg/engine/jsonContext.go index 859d26387b..93970abe5a 100644 --- a/pkg/engine/jsonContext.go +++ b/pkg/engine/jsonContext.go @@ -17,13 +17,14 @@ import ( "github.com/kyverno/kyverno/pkg/logging" "github.com/kyverno/kyverno/pkg/registryclient" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" ) -type ContextLoaderFactory = func(pContext *PolicyContext, ruleName string) engineapi.ContextLoader +type ContextLoaderFactory = func(pContext engineapi.PolicyContext, ruleName string) engineapi.ContextLoader func LegacyContextLoaderFactory(rclient registryclient.Client) ContextLoaderFactory { if store.IsMock() { - return func(pContext *PolicyContext, ruleName string) engineapi.ContextLoader { + return func(pContext engineapi.PolicyContext, ruleName string) engineapi.ContextLoader { policy := pContext.Policy() return &mockContextLoader{ logger: logging.WithName("MockContextLoaderFactory"), @@ -31,16 +32,16 @@ func LegacyContextLoaderFactory(rclient registryclient.Client) ContextLoaderFact ruleName: ruleName, client: pContext.Client(), rclient: rclient, - cmResolver: pContext.informerCacheResolvers, + cmResolver: pContext.ResolveConfigMap, } } } - return func(pContext *PolicyContext, ruleName string) engineapi.ContextLoader { + return func(pContext engineapi.PolicyContext, ruleName string) engineapi.ContextLoader { return &contextLoader{ logger: logging.WithName("LegacyContextLoaderFactory"), client: pContext.Client(), rclient: rclient, - cmResolver: pContext.informerCacheResolvers, + cmResolver: pContext.ResolveConfigMap, } } } @@ -49,7 +50,7 @@ type contextLoader struct { logger logr.Logger rclient registryclient.Client client dclient.Interface - cmResolver engineapi.ConfigmapResolver + cmResolver func(context.Context, string, string) (*corev1.ConfigMap, error) } func (l *contextLoader) Load(ctx context.Context, contextEntries []kyvernov1.ContextEntry, enginectx enginecontext.Interface) error { @@ -81,7 +82,7 @@ type mockContextLoader struct { ruleName string rclient registryclient.Client client dclient.Interface - cmResolver engineapi.ConfigmapResolver + cmResolver func(context.Context, string, string) (*corev1.ConfigMap, error) } func (l *mockContextLoader) Load(ctx context.Context, contextEntries []kyvernov1.ContextEntry, enginectx enginecontext.Interface) error { @@ -122,7 +123,7 @@ func (l *mockContextLoader) Load(ctx context.Context, contextEntries []kyvernov1 return nil } -func LoadContext(ctx context.Context, factory ContextLoaderFactory, contextEntries []kyvernov1.ContextEntry, pContext *PolicyContext, ruleName string) error { +func LoadContext(ctx context.Context, factory ContextLoaderFactory, contextEntries []kyvernov1.ContextEntry, pContext engineapi.PolicyContext, ruleName string) error { return factory(pContext, ruleName).Load(ctx, contextEntries, pContext.JSONContext()) } @@ -300,7 +301,7 @@ func applyJMESPath(jmesPath string, data interface{}) (interface{}, error) { return jp.Search(data) } -func loadConfigMap(ctx context.Context, logger logr.Logger, entry kyvernov1.ContextEntry, enginectx enginecontext.Interface, resolver engineapi.ConfigmapResolver) error { +func loadConfigMap(ctx context.Context, logger logr.Logger, entry kyvernov1.ContextEntry, enginectx enginecontext.Interface, resolver func(context.Context, string, string) (*corev1.ConfigMap, error)) error { data, err := fetchConfigMap(ctx, logger, entry, enginectx, resolver) if err != nil { return fmt.Errorf("failed to retrieve config map for context entry %s: %v", entry.Name, err) @@ -312,7 +313,7 @@ func loadConfigMap(ctx context.Context, logger logr.Logger, entry kyvernov1.Cont return nil } -func fetchConfigMap(ctx context.Context, logger logr.Logger, entry kyvernov1.ContextEntry, enginectx enginecontext.Interface, resolver engineapi.ConfigmapResolver) ([]byte, error) { +func fetchConfigMap(ctx context.Context, logger logr.Logger, entry kyvernov1.ContextEntry, enginectx enginecontext.Interface, resolver func(context.Context, string, string) (*corev1.ConfigMap, error)) ([]byte, error) { contextData := make(map[string]interface{}) name, err := variables.SubstituteAll(logger, enginectx, entry.ConfigMap.Name) @@ -329,7 +330,7 @@ func fetchConfigMap(ctx context.Context, logger logr.Logger, entry kyvernov1.Con namespace = "default" } - obj, err := resolver.Get(ctx, namespace.(string), name.(string)) + obj, err := resolver(ctx, namespace.(string), name.(string)) if err != nil { return nil, fmt.Errorf("failed to get configmap %s/%s : %v", namespace, name, err) } diff --git a/pkg/engine/k8smanifest.go b/pkg/engine/k8smanifest.go index aeaed8a699..e23a3b25b2 100644 --- a/pkg/engine/k8smanifest.go +++ b/pkg/engine/k8smanifest.go @@ -35,7 +35,7 @@ const ( //go:embed resources/default-config.yaml var defaultConfigBytes []byte -func processYAMLValidationRule(log logr.Logger, ctx *PolicyContext, rule *kyvernov1.Rule) *engineapi.RuleResponse { +func processYAMLValidationRule(log logr.Logger, ctx engineapi.PolicyContext, rule *kyvernov1.Rule) *engineapi.RuleResponse { if isDeleteRequest(ctx) { return nil } @@ -43,7 +43,7 @@ func processYAMLValidationRule(log logr.Logger, ctx *PolicyContext, rule *kyvern return ruleResp } -func handleVerifyManifest(ctx *PolicyContext, rule *kyvernov1.Rule, logger logr.Logger) *engineapi.RuleResponse { +func handleVerifyManifest(ctx engineapi.PolicyContext, rule *kyvernov1.Rule, logger logr.Logger) *engineapi.RuleResponse { verified, reason, err := verifyManifest(ctx, *rule.Validation.Manifests, logger) if err != nil { logger.V(3).Info("verifyManifest return err", "error", err.Error()) @@ -56,9 +56,9 @@ func handleVerifyManifest(ctx *PolicyContext, rule *kyvernov1.Rule, logger logr. return ruleResponse(*rule, engineapi.Validation, reason, engineapi.RuleStatusPass) } -func verifyManifest(policyContext *PolicyContext, verifyRule kyvernov1.Manifests, logger logr.Logger) (bool, string, error) { +func verifyManifest(policyContext engineapi.PolicyContext, verifyRule kyvernov1.Manifests, logger logr.Logger) (bool, string, error) { // load AdmissionRequest - request, err := policyContext.jsonContext.Query("request") + request, err := policyContext.JSONContext().Query("request") if err != nil { return false, "", errors.Wrapf(err, "failed to get a request from policyContext") } @@ -106,7 +106,7 @@ func verifyManifest(policyContext *PolicyContext, verifyRule kyvernov1.Manifests } if !vo.DisableDryRun { // check if kyverno can 'create' dryrun resource - ok, err := checkDryRunPermission(policyContext.client, adreq.Kind.Kind, vo.DryRunNamespace) + ok, err := checkDryRunPermission(policyContext.Client(), adreq.Kind.Kind, vo.DryRunNamespace) if err != nil { logger.V(1).Info("failed to check permissions to 'create' resource. disabled DryRun option.", "dryrun namespace", vo.DryRunNamespace, "kind", adreq.Kind.Kind, "error", err.Error()) vo.DisableDryRun = true diff --git a/pkg/engine/loadtargets.go b/pkg/engine/loadtargets.go index 7e17541166..14efad7e3e 100644 --- a/pkg/engine/loadtargets.go +++ b/pkg/engine/loadtargets.go @@ -7,6 +7,7 @@ import ( "github.com/go-logr/logr" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" + engineapi "github.com/kyverno/kyverno/pkg/engine/api" "github.com/kyverno/kyverno/pkg/engine/variables" kubeutils "github.com/kyverno/kyverno/pkg/utils/kube" "github.com/kyverno/kyverno/pkg/utils/wildcard" @@ -23,7 +24,7 @@ type resourceInfo struct { parentResourceGVR metav1.GroupVersionResource } -func loadTargets(targets []kyvernov1.ResourceSpec, ctx *PolicyContext, logger logr.Logger) ([]resourceInfo, error) { +func loadTargets(targets []kyvernov1.ResourceSpec, ctx engineapi.PolicyContext, logger logr.Logger) ([]resourceInfo, error) { var targetObjects []resourceInfo var errors []error @@ -46,23 +47,23 @@ func loadTargets(targets []kyvernov1.ResourceSpec, ctx *PolicyContext, logger lo return targetObjects, multierr.Combine(errors...) } -func resolveSpec(i int, target kyvernov1.ResourceSpec, ctx *PolicyContext, logger logr.Logger) (kyvernov1.ResourceSpec, error) { - kind, err := variables.SubstituteAll(logger, ctx.jsonContext, target.Kind) +func resolveSpec(i int, target kyvernov1.ResourceSpec, ctx engineapi.PolicyContext, logger logr.Logger) (kyvernov1.ResourceSpec, error) { + kind, err := variables.SubstituteAll(logger, ctx.JSONContext(), target.Kind) if err != nil { return kyvernov1.ResourceSpec{}, fmt.Errorf("failed to substitute variables in target[%d].Kind %s: %v", i, target.Kind, err) } - apiversion, err := variables.SubstituteAll(logger, ctx.jsonContext, target.APIVersion) + apiversion, err := variables.SubstituteAll(logger, ctx.JSONContext(), target.APIVersion) if err != nil { return kyvernov1.ResourceSpec{}, fmt.Errorf("failed to substitute variables in target[%d].APIVersion %s: %v", i, target.APIVersion, err) } - namespace, err := variables.SubstituteAll(logger, ctx.jsonContext, target.Namespace) + namespace, err := variables.SubstituteAll(logger, ctx.JSONContext(), target.Namespace) if err != nil { return kyvernov1.ResourceSpec{}, fmt.Errorf("failed to substitute variables in target[%d].Namespace %s: %v", i, target.Namespace, err) } - name, err := variables.SubstituteAll(logger, ctx.jsonContext, target.Name) + name, err := variables.SubstituteAll(logger, ctx.JSONContext(), target.Name) if err != nil { return kyvernov1.ResourceSpec{}, fmt.Errorf("failed to substitute variables in target[%d].Name %s: %v", i, target.Name, err) } @@ -75,17 +76,18 @@ func resolveSpec(i int, target kyvernov1.ResourceSpec, ctx *PolicyContext, logge }, nil } -func getTargets(target kyvernov1.ResourceSpec, ctx *PolicyContext) ([]resourceInfo, error) { +func getTargets(target kyvernov1.ResourceSpec, ctx engineapi.PolicyContext) ([]resourceInfo, error) { var targetObjects []resourceInfo namespace := target.Namespace name := target.Name + policy := ctx.Policy() // if it's namespaced policy, targets has to be loaded only from the policy's namespace - if ctx.policy.IsNamespaced() { - namespace = ctx.policy.GetNamespace() + if policy.IsNamespaced() { + namespace = policy.GetNamespace() } - - apiResource, parentAPIResource, _, err := ctx.client.Discovery().FindResource(target.APIVersion, target.Kind) + client := ctx.Client() + apiResource, parentAPIResource, _, err := client.Discovery().FindResource(target.APIVersion, target.Kind) if err != nil { return nil, err } @@ -102,14 +104,14 @@ func getTargets(target kyvernov1.ResourceSpec, ctx *PolicyContext) ([]resourceIn Version: parentAPIResource.Version, }.String() subresourceName = strings.Split(apiResource.Name, "/")[1] - obj, err = ctx.client.GetResource(context.TODO(), apiVersion, parentAPIResource.Kind, namespace, name, subresourceName) + obj, err = client.GetResource(context.TODO(), apiVersion, parentAPIResource.Kind, namespace, name, subresourceName) parentResourceGVR = metav1.GroupVersionResource{ Group: parentAPIResource.Group, Version: parentAPIResource.Version, Resource: parentAPIResource.Name, } } else { - obj, err = ctx.client.GetResource(context.TODO(), target.APIVersion, target.Kind, namespace, name) + obj, err = client.GetResource(context.TODO(), target.APIVersion, target.Kind, namespace, name) } if err != nil { return nil, fmt.Errorf("failed to get target %s/%s %s/%s : %v", target.APIVersion, target.Kind, namespace, name, err) @@ -123,7 +125,7 @@ func getTargets(target kyvernov1.ResourceSpec, ctx *PolicyContext) ([]resourceIn Group: parentAPIResource.Group, Version: parentAPIResource.Version, }.String() - objList, err := ctx.client.ListResource(context.TODO(), apiVersion, parentAPIResource.Kind, "", nil) + objList, err := client.ListResource(context.TODO(), apiVersion, parentAPIResource.Kind, "", nil) if err != nil { return nil, err } @@ -138,7 +140,7 @@ func getTargets(target kyvernov1.ResourceSpec, ctx *PolicyContext) ([]resourceIn for i := range parentObjects { parentObj := parentObjects[i] subresourceName := strings.Split(apiResource.Name, "/")[1] - obj, err := ctx.client.GetResource(context.TODO(), parentObj.GetAPIVersion(), parentAPIResource.Kind, parentObj.GetNamespace(), parentObj.GetName(), subresourceName) + obj, err := client.GetResource(context.TODO(), parentObj.GetAPIVersion(), parentAPIResource.Kind, parentObj.GetNamespace(), parentObj.GetName(), subresourceName) if err != nil { return nil, err } @@ -151,7 +153,7 @@ func getTargets(target kyvernov1.ResourceSpec, ctx *PolicyContext) ([]resourceIn } } else { // list all targets if wildcard is specified - objList, err := ctx.client.ListResource(context.TODO(), target.APIVersion, target.Kind, "", nil) + objList, err := client.ListResource(context.TODO(), target.APIVersion, target.Kind, "", nil) if err != nil { return nil, err } diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index 1a923f5bed..914314adfe 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -24,15 +24,15 @@ import ( func Mutate( ctx context.Context, contextLoader ContextLoaderFactory, - policyContext *PolicyContext, + policyContext engineapi.PolicyContext, ) (resp *engineapi.EngineResponse) { startTime := time.Now() - policy := policyContext.policy + policy := policyContext.Policy() resp = &engineapi.EngineResponse{ Policy: policy, } - matchedResource := policyContext.newResource - enginectx := policyContext.jsonContext + matchedResource := policyContext.NewResource() + enginectx := policyContext.JSONContext() var skippedRules []string logger := logging.WithName("EngineMutate").WithValues("policy", policy.GetName(), "kind", matchedResource.GetKind(), @@ -43,8 +43,8 @@ func Mutate( startMutateResultResponse(resp, policy, matchedResource) defer endMutateResultResponse(logger, resp, startTime) - policyContext.jsonContext.Checkpoint() - defer policyContext.jsonContext.Restore() + policyContext.JSONContext().Checkpoint() + defer policyContext.JSONContext().Restore() var err error applyRules := policy.GetSpec().GetApplyRules() @@ -62,13 +62,13 @@ func Mutate( func(ctx context.Context, span trace.Span) { logger := logger.WithValues("rule", rule.Name) var excludeResource []string - if len(policyContext.excludeGroupRole) > 0 { - excludeResource = policyContext.excludeGroupRole + if len(policyContext.ExcludeGroupRole()) > 0 { + excludeResource = policyContext.ExcludeGroupRole() } kindsInPolicy := append(rule.MatchResources.GetKinds(), rule.ExcludeResources.GetKinds()...) subresourceGVKToAPIResource := GetSubresourceGVKToAPIResourceMap(kindsInPolicy, policyContext) - if err = MatchesResourceDescription(subresourceGVKToAPIResource, matchedResource, rule, policyContext.admissionInfo, excludeResource, policyContext.namespaceLabels, policyContext.policy.GetNamespace(), policyContext.subresource); err != nil { + if err = MatchesResourceDescription(subresourceGVKToAPIResource, matchedResource, rule, policyContext.AdmissionInfo(), excludeResource, policyContext.NamespaceLabels(), policyContext.Policy().GetNamespace(), policyContext.SubResource()); err != nil { logger.V(4).Info("rule not matched", "reason", err.Error()) skippedRules = append(skippedRules, rule.Name) return @@ -82,8 +82,8 @@ func Mutate( } logger.V(3).Info("processing mutate rule", "applyRules", applyRules) - resource, err := policyContext.jsonContext.Query("request.object") - policyContext.jsonContext.Reset() + resource, err := policyContext.JSONContext().Query("request.object") + policyContext.JSONContext().Reset() if err == nil && resource != nil { if err := enginectx.AddResource(resource.(map[string]interface{})); err != nil { logger.Error(err, "unable to update resource object") @@ -103,7 +103,7 @@ func Mutate( ruleCopy := rule.DeepCopy() var patchedResources []resourceInfo - if !policyContext.admissionOperation && rule.IsMutateExisting() { + if !policyContext.AdmissionOperation() && rule.IsMutateExisting() { targets, err := loadTargets(ruleCopy.Mutation.Targets, policyContext, logger) if err != nil { rr := ruleResponse(rule, engineapi.Mutation, err.Error(), engineapi.RuleStatusError) @@ -113,12 +113,12 @@ func Mutate( } } else { var parentResourceGVR metav1.GroupVersionResource - if policyContext.subresource != "" { - parentResourceGVR = policyContext.requestResource + if policyContext.SubResource() != "" { + parentResourceGVR = policyContext.RequestResource() } patchedResources = append(patchedResources, resourceInfo{ unstructured: matchedResource, - subresource: policyContext.subresource, + subresource: policyContext.SubResource(), parentResourceGVR: parentResourceGVR, }) } @@ -128,9 +128,9 @@ func Mutate( continue } - if !policyContext.admissionOperation && rule.IsMutateExisting() { + if !policyContext.AdmissionOperation() && rule.IsMutateExisting() { policyContext := policyContext.Copy() - if err := policyContext.jsonContext.AddTargetResource(patchedResource.unstructured.Object); err != nil { + if err := policyContext.JSONContext().AddTargetResource(patchedResource.unstructured.Object); err != nil { logging.Error(err, "failed to add target resource to the context") continue } @@ -186,7 +186,7 @@ func Mutate( return resp } -func mutateResource(rule *kyvernov1.Rule, ctx *PolicyContext, resource unstructured.Unstructured, logger logr.Logger) *mutate.Response { +func mutateResource(rule *kyvernov1.Rule, ctx engineapi.PolicyContext, resource unstructured.Unstructured, logger logr.Logger) *mutate.Response { preconditionsPassed, err := checkPreconditions(logger, ctx, rule.GetAnyAllConditions()) if err != nil { return mutate.NewErrorResponse("failed to evaluate preconditions", err) @@ -201,7 +201,7 @@ func mutateResource(rule *kyvernov1.Rule, ctx *PolicyContext, resource unstructu type forEachMutator struct { rule *kyvernov1.Rule - policyContext *PolicyContext + policyContext engineapi.PolicyContext foreach []kyvernov1.ForEachMutation resource resourceInfo nesting int diff --git a/pkg/engine/policyContext.go b/pkg/engine/policyContext.go index 0d3454093b..36b012063c 100644 --- a/pkg/engine/policyContext.go +++ b/pkg/engine/policyContext.go @@ -1,25 +1,25 @@ package engine import ( + "context" + kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1" kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1" "github.com/kyverno/kyverno/pkg/clients/dclient" "github.com/kyverno/kyverno/pkg/config" - "github.com/kyverno/kyverno/pkg/engine/api" + engineapi "github.com/kyverno/kyverno/pkg/engine/api" enginectx "github.com/kyverno/kyverno/pkg/engine/context" admissionutils "github.com/kyverno/kyverno/pkg/utils/admission" "github.com/pkg/errors" admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/tools/cache" ) -// ExcludeFunc is a function used to determine if a resource is excluded -type ExcludeFunc = func(kind, namespace, name string) bool - type PolicyExceptionLister interface { // List lists all PolicyExceptions in the indexer. // Objects returned here must be treated as read-only. @@ -59,7 +59,7 @@ type PolicyContext struct { // Config handler excludeGroupRole []string - excludeResourceFunc ExcludeFunc + excludeResourceFunc engineapi.ExcludeFunc // jsonContext is the variable context jsonContext enginectx.Interface @@ -71,7 +71,7 @@ type PolicyContext struct { admissionOperation bool // informerCacheResolvers - used to get resources from informer cache - informerCacheResolvers api.ConfigmapResolver + informerCacheResolvers engineapi.ConfigmapResolver // subresource is the subresource being requested, if any (for example, "status" or "scale") subresource string @@ -79,16 +79,13 @@ type PolicyContext struct { // subresourcesInPolicy represents the APIResources that are subresources along with their parent resource. // This is used to determine if a resource is a subresource. It is only used when the policy context is populated // by kyverno CLI. In all other cases when connected to a cluster, this is empty. - subresourcesInPolicy []struct { - APIResource metav1.APIResource - ParentResource metav1.APIResource - } + subresourcesInPolicy []engineapi.SubResource // peLister list all policy exceptions peLister PolicyExceptionLister } -// Getters +// engineapi.PolicyContext interface func (c *PolicyContext) Policy() kyvernov1.PolicyInterface { return c.policy @@ -106,10 +103,50 @@ func (c *PolicyContext) AdmissionInfo() kyvernov1beta1.RequestInfo { return c.admissionInfo } +func (c *PolicyContext) NamespaceLabels() map[string]string { + return c.namespaceLabels +} + +func (c *PolicyContext) SubResource() string { + return c.subresource +} + +func (c *PolicyContext) SubresourcesInPolicy() []engineapi.SubResource { + return c.subresourcesInPolicy +} + +func (c *PolicyContext) ExcludeGroupRole() []string { + return c.excludeGroupRole +} + +func (c *PolicyContext) AdmissionOperation() bool { + return c.admissionOperation +} + +func (c *PolicyContext) RequestResource() metav1.GroupVersionResource { + return c.requestResource +} + +func (c *PolicyContext) Element() unstructured.Unstructured { + return c.element +} + +func (c *PolicyContext) SetElement(element unstructured.Unstructured) { + c.element = element +} + func (c *PolicyContext) JSONContext() enginectx.Interface { return c.jsonContext } +func (c *PolicyContext) Client() dclient.Interface { + return c.client +} + +func (c PolicyContext) Copy() engineapi.PolicyContext { + return c.copy() +} + func (c *PolicyContext) FindExceptions(rule string) ([]*kyvernov2alpha1.PolicyException, error) { if c.peLister == nil { return nil, nil @@ -131,44 +168,48 @@ func (c *PolicyContext) FindExceptions(rule string) ([]*kyvernov2alpha1.PolicyEx return result, nil } -func (c *PolicyContext) Client() dclient.Interface { - return c.client +func (c *PolicyContext) ExcludeResourceFunc() engineapi.ExcludeFunc { + return c.excludeResourceFunc +} + +func (c *PolicyContext) ResolveConfigMap(ctx context.Context, namespace string, name string) (*corev1.ConfigMap, error) { + return c.informerCacheResolvers.Get(ctx, namespace, name) } // Mutators func (c *PolicyContext) WithPolicy(policy kyvernov1.PolicyInterface) *PolicyContext { - copy := c.Copy() + copy := c.copy() copy.policy = policy return copy } func (c *PolicyContext) WithNamespaceLabels(namespaceLabels map[string]string) *PolicyContext { - copy := c.Copy() + copy := c.copy() copy.namespaceLabels = namespaceLabels return copy } func (c *PolicyContext) WithAdmissionInfo(admissionInfo kyvernov1beta1.RequestInfo) *PolicyContext { - copy := c.Copy() + copy := c.copy() copy.admissionInfo = admissionInfo return copy } func (c *PolicyContext) WithRequestResource(requestResource metav1.GroupVersionResource) *PolicyContext { - copy := c.Copy() + copy := c.copy() copy.requestResource = requestResource return copy } func (c *PolicyContext) WithNewResource(resource unstructured.Unstructured) *PolicyContext { - copy := c.Copy() + copy := c.copy() copy.newResource = resource return copy } func (c *PolicyContext) WithOldResource(resource unstructured.Unstructured) *PolicyContext { - copy := c.Copy() + copy := c.copy() copy.oldResource = resource return copy } @@ -178,19 +219,19 @@ func (c *PolicyContext) WithResources(newResource unstructured.Unstructured, old } func (c *PolicyContext) WithClient(client dclient.Interface) *PolicyContext { - copy := c.Copy() + copy := c.copy() copy.client = client return copy } func (c *PolicyContext) WithExcludeGroupRole(excludeGroupRole ...string) *PolicyContext { - copy := c.Copy() + copy := c.copy() copy.excludeGroupRole = excludeGroupRole return copy } -func (c *PolicyContext) WithExcludeResourceFunc(excludeResourceFunc ExcludeFunc) *PolicyContext { - copy := c.Copy() +func (c *PolicyContext) WithExcludeResourceFunc(excludeResourceFunc engineapi.ExcludeFunc) *PolicyContext { + copy := c.copy() copy.excludeResourceFunc = excludeResourceFunc return copy } @@ -200,39 +241,39 @@ func (c *PolicyContext) WithConfiguration(configuration config.Configuration) *P } func (c *PolicyContext) WithAdmissionOperation(admissionOperation bool) *PolicyContext { - copy := c.Copy() + copy := c.copy() copy.admissionOperation = admissionOperation return copy } -func (c *PolicyContext) WithInformerCacheResolver(informerCacheResolver api.ConfigmapResolver) *PolicyContext { - copy := c.Copy() +func (c *PolicyContext) WithInformerCacheResolver(informerCacheResolver engineapi.ConfigmapResolver) *PolicyContext { + copy := c.copy() copy.informerCacheResolvers = informerCacheResolver return copy } func (c *PolicyContext) WithSubresource(subresource string) *PolicyContext { - copy := c.Copy() + copy := c.copy() copy.subresource = subresource return copy } -func (c *PolicyContext) WithSubresourcesInPolicy(subresourcesInPolicy []struct { - APIResource metav1.APIResource - ParentResource metav1.APIResource -}, -) *PolicyContext { - copy := c.Copy() +func (c *PolicyContext) WithSubresourcesInPolicy(subresourcesInPolicy []engineapi.SubResource) *PolicyContext { + copy := c.copy() copy.subresourcesInPolicy = subresourcesInPolicy return copy } func (c *PolicyContext) WithExceptions(peLister PolicyExceptionLister) *PolicyContext { - copy := c.Copy() + copy := c.copy() copy.peLister = peLister return copy } +func (c PolicyContext) copy() *PolicyContext { + return &c +} + // Constructors func NewPolicyContextWithJsonContext(jsonContext enginectx.Interface) *PolicyContext { return &PolicyContext{ @@ -253,7 +294,7 @@ func NewPolicyContextFromAdmissionRequest( admissionInfo kyvernov1beta1.RequestInfo, configuration config.Configuration, client dclient.Interface, - informerCacheResolver api.ConfigmapResolver, + informerCacheResolver engineapi.ConfigmapResolver, polexLister PolicyExceptionLister, ) (*PolicyContext, error) { ctx, err := newVariablesContext(request, &admissionInfo) @@ -282,10 +323,6 @@ func NewPolicyContextFromAdmissionRequest( return policyContext, nil } -func (c PolicyContext) Copy() *PolicyContext { - return &c -} - func newVariablesContext(request *admissionv1.AdmissionRequest, userRequestInfo *kyvernov1beta1.RequestInfo) (enginectx.Interface, error) { ctx := enginectx.NewContext() if err := ctx.AddRequest(request); err != nil { diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index e918797f62..f67652e4df 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -325,8 +325,8 @@ func ManagedPodResource(policy kyvernov1.PolicyInterface, resource unstructured. return false } -func checkPreconditions(logger logr.Logger, ctx *PolicyContext, anyAllConditions apiextensions.JSON) (bool, error) { - preconditions, err := variables.SubstituteAllInPreconditions(logger, ctx.jsonContext, anyAllConditions) +func checkPreconditions(logger logr.Logger, ctx engineapi.PolicyContext, anyAllConditions apiextensions.JSON) (bool, error) { + preconditions, err := variables.SubstituteAllInPreconditions(logger, ctx.JSONContext(), anyAllConditions) if err != nil { return false, errors.Wrapf(err, "failed to substitute variables in preconditions") } @@ -336,7 +336,7 @@ func checkPreconditions(logger logr.Logger, ctx *PolicyContext, anyAllConditions return false, errors.Wrapf(err, "failed to parse preconditions") } - pass := variables.EvaluateConditions(logger, ctx.jsonContext, typeConditions) + pass := variables.EvaluateConditions(logger, ctx.JSONContext(), typeConditions) return pass, nil } diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 260423f638..523f9bb069 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -39,7 +39,7 @@ import ( func Validate( ctx context.Context, contextLoader ContextLoaderFactory, - policyContext *PolicyContext, + policyContext engineapi.PolicyContext, cfg config.Configuration, ) (resp *engineapi.EngineResponse) { resp = &engineapi.EngineResponse{} @@ -53,46 +53,48 @@ func Validate( }() resp = validateResource(ctx, contextLoader, logger, policyContext, cfg) - resp.NamespaceLabels = policyContext.namespaceLabels + resp.NamespaceLabels = policyContext.NamespaceLabels() return } -func buildLogger(ctx *PolicyContext) logr.Logger { - logger := logging.WithName("EngineValidate").WithValues("policy", ctx.policy.GetName()) - if reflect.DeepEqual(ctx.newResource, unstructured.Unstructured{}) { - logger = logger.WithValues("kind", ctx.oldResource.GetKind(), "namespace", ctx.oldResource.GetNamespace(), "name", ctx.oldResource.GetName()) +func buildLogger(ctx engineapi.PolicyContext) logr.Logger { + logger := logging.WithName("EngineValidate").WithValues("policy", ctx.Policy().GetName()) + newResource := ctx.NewResource() + oldResource := ctx.OldResource() + if reflect.DeepEqual(newResource, unstructured.Unstructured{}) { + logger = logger.WithValues("kind", oldResource.GetKind(), "namespace", oldResource.GetNamespace(), "name", oldResource.GetName()) } else { - logger = logger.WithValues("kind", ctx.newResource.GetKind(), "namespace", ctx.newResource.GetNamespace(), "name", ctx.newResource.GetName()) + logger = logger.WithValues("kind", newResource.GetKind(), "namespace", newResource.GetNamespace(), "name", newResource.GetName()) } return logger } -func buildResponse(ctx *PolicyContext, resp *engineapi.EngineResponse, startTime time.Time) { +func buildResponse(ctx engineapi.PolicyContext, resp *engineapi.EngineResponse, startTime time.Time) { if reflect.DeepEqual(resp, engineapi.EngineResponse{}) { return } if reflect.DeepEqual(resp.PatchedResource, unstructured.Unstructured{}) { // for delete requests patched resource will be oldResource since newResource is empty - resource := ctx.newResource - if reflect.DeepEqual(ctx.newResource, unstructured.Unstructured{}) { - resource = ctx.oldResource + resource := ctx.NewResource() + if reflect.DeepEqual(resource, unstructured.Unstructured{}) { + resource = ctx.OldResource() } resp.PatchedResource = resource } - - resp.Policy = ctx.policy - resp.PolicyResponse.Policy.Name = ctx.policy.GetName() - resp.PolicyResponse.Policy.Namespace = ctx.policy.GetNamespace() + policy := ctx.Policy() + resp.Policy = policy + resp.PolicyResponse.Policy.Name = policy.GetName() + resp.PolicyResponse.Policy.Namespace = policy.GetNamespace() resp.PolicyResponse.Resource.Name = resp.PatchedResource.GetName() resp.PolicyResponse.Resource.Namespace = resp.PatchedResource.GetNamespace() resp.PolicyResponse.Resource.Kind = resp.PatchedResource.GetKind() resp.PolicyResponse.Resource.APIVersion = resp.PatchedResource.GetAPIVersion() - resp.PolicyResponse.ValidationFailureAction = ctx.policy.GetSpec().ValidationFailureAction + resp.PolicyResponse.ValidationFailureAction = policy.GetSpec().ValidationFailureAction - for _, v := range ctx.policy.GetSpec().ValidationFailureActionOverrides { + for _, v := range policy.GetSpec().ValidationFailureActionOverrides { newOverrides := engineapi.ValidationFailureActionOverride{Action: v.Action, Namespaces: v.Namespaces, NamespaceSelector: v.NamespaceSelector} resp.PolicyResponse.ValidationFailureActionOverrides = append(resp.PolicyResponse.ValidationFailureActionOverrides, newOverrides) } @@ -105,24 +107,26 @@ func validateResource( ctx context.Context, contextLoader ContextLoaderFactory, log logr.Logger, - enginectx *PolicyContext, + enginectx engineapi.PolicyContext, cfg config.Configuration, ) *engineapi.EngineResponse { resp := &engineapi.EngineResponse{} - enginectx.jsonContext.Checkpoint() - defer enginectx.jsonContext.Restore() + enginectx.JSONContext().Checkpoint() + defer enginectx.JSONContext().Restore() - rules := autogen.ComputeRules(enginectx.policy) + rules := autogen.ComputeRules(enginectx.Policy()) matchCount := 0 - applyRules := enginectx.policy.GetSpec().GetApplyRules() + applyRules := enginectx.Policy().GetSpec().GetApplyRules() + newResource := enginectx.NewResource() + oldResource := enginectx.OldResource() - if enginectx.policy.IsNamespaced() { - polNs := enginectx.policy.GetNamespace() - if enginectx.newResource.Object != nil && (enginectx.newResource.GetNamespace() != polNs || enginectx.newResource.GetNamespace() == "") { + if enginectx.Policy().IsNamespaced() { + polNs := enginectx.Policy().GetNamespace() + if enginectx.NewResource().Object != nil && (newResource.GetNamespace() != polNs || newResource.GetNamespace() == "") { return resp } - if enginectx.oldResource.Object != nil && (enginectx.oldResource.GetNamespace() != polNs || enginectx.oldResource.GetNamespace() == "") { + if enginectx.OldResource().Object != nil && (oldResource.GetNamespace() != polNs || oldResource.GetNamespace() == "") { return resp } } @@ -130,7 +134,7 @@ func validateResource( for i := range rules { rule := &rules[i] log.V(3).Info("processing validation rule", "matchCount", matchCount, "applyRules", applyRules) - enginectx.jsonContext.Reset() + enginectx.JSONContext().Reset() startTime := time.Now() ruleResp := tracing.ChildSpan1( ctx, @@ -156,7 +160,7 @@ func validateResource( return ruleResp } log.V(3).Info("processing validation rule", "matchCount", matchCount, "applyRules", applyRules) - enginectx.jsonContext.Reset() + enginectx.JSONContext().Reset() if hasValidate && !hasYAMLSignatureVerify { return processValidationRule(ctx, contextLoader, log, enginectx, rule) } else if hasValidateImage { @@ -182,7 +186,7 @@ func processValidationRule( ctx context.Context, contextLoader ContextLoaderFactory, log logr.Logger, - policyContext *PolicyContext, + policyContext engineapi.PolicyContext, rule *kyvernov1.Rule, ) *engineapi.RuleResponse { v := newValidator(log, contextLoader, policyContext, rule) @@ -205,7 +209,7 @@ func addRuleResponse(log logr.Logger, resp *engineapi.EngineResponse, ruleResp * type validator struct { log logr.Logger - policyContext *PolicyContext + policyContext engineapi.PolicyContext rule *kyvernov1.Rule contextEntries []kyvernov1.ContextEntry anyAllConditions apiextensions.JSON @@ -218,7 +222,7 @@ type validator struct { nesting int } -func newValidator(log logr.Logger, contextLoader ContextLoaderFactory, ctx *PolicyContext, rule *kyvernov1.Rule) *validator { +func newValidator(log logr.Logger, contextLoader ContextLoaderFactory, ctx engineapi.PolicyContext, rule *kyvernov1.Rule) *validator { ruleCopy := rule.DeepCopy() return &validator{ log: log, @@ -240,7 +244,7 @@ func newForEachValidator( contextLoader ContextLoaderFactory, nesting int, rule *kyvernov1.Rule, - ctx *PolicyContext, + ctx engineapi.PolicyContext, log logr.Logger, ) (*validator, error) { ruleCopy := rule.DeepCopy() @@ -325,24 +329,20 @@ func (v *validator) validateForEach(ctx context.Context) *engineapi.RuleResponse if resp.Status != engineapi.RuleStatusPass { return resp } - applyCount += count } - if applyCount == 0 { if v.forEach == nil { return nil } - return ruleResponse(*v.rule, engineapi.Validation, "rule skipped", engineapi.RuleStatusSkip) } - return ruleResponse(*v.rule, engineapi.Validation, "rule passed", engineapi.RuleStatusPass) } func (v *validator) validateElements(ctx context.Context, foreach kyvernov1.ForEachValidation, elements []interface{}, elementScope *bool) (*engineapi.RuleResponse, int) { - v.policyContext.jsonContext.Checkpoint() - defer v.policyContext.jsonContext.Restore() + v.policyContext.JSONContext().Checkpoint() + defer v.policyContext.JSONContext().Restore() applyCount := 0 for index, element := range elements { @@ -388,7 +388,7 @@ func (v *validator) validateElements(ctx context.Context, foreach kyvernov1.ForE return ruleResponse(*v.rule, engineapi.Validation, "", engineapi.RuleStatusPass), applyCount } -func addElementToContext(ctx *PolicyContext, element interface{}, index, nesting int, elementScope *bool) error { +func addElementToContext(ctx engineapi.PolicyContext, element interface{}, index, nesting int, elementScope *bool) error { data, err := variables.DocumentToUntyped(element) if err != nil { return err @@ -412,11 +412,10 @@ func addElementToContext(ctx *PolicyContext, element interface{}, index, nesting } scoped = *elementScope } - if scoped { u := unstructured.Unstructured{} u.SetUnstructuredContent(dataMap) - ctx.element = u + ctx.SetElement(u) } return nil } @@ -437,7 +436,7 @@ func (v *validator) loadContext(ctx context.Context) error { func (v *validator) validateDeny() *engineapi.RuleResponse { anyAllCond := v.deny.GetAnyAllConditions() - anyAllCond, err := variables.SubstituteAll(v.log, v.policyContext.jsonContext, anyAllCond) + anyAllCond, err := variables.SubstituteAll(v.log, v.policyContext.JSONContext(), anyAllCond) if err != nil { return ruleError(v.rule, engineapi.Validation, "failed to substitute variables in deny conditions", err) } @@ -451,7 +450,7 @@ func (v *validator) validateDeny() *engineapi.RuleResponse { return ruleError(v.rule, engineapi.Validation, "invalid deny conditions", err) } - deny := variables.EvaluateConditions(v.log, v.policyContext.jsonContext, denyConditions) + deny := variables.EvaluateConditions(v.log, v.policyContext.JSONContext(), denyConditions) if deny { return ruleResponse(*v.rule, engineapi.Validation, v.getDenyMessage(deny), engineapi.RuleStatusFail) } @@ -467,7 +466,7 @@ func (v *validator) getDenyMessage(deny bool) string { if msg == "" { return fmt.Sprintf("validation error: rule %s failed", v.rule.Name) } - raw, err := variables.SubstituteAll(v.log, v.policyContext.jsonContext, msg) + raw, err := variables.SubstituteAll(v.log, v.policyContext.JSONContext(), msg) if err != nil { return msg } @@ -480,12 +479,13 @@ func (v *validator) getDenyMessage(deny bool) string { } func getSpec(v *validator) (podSpec *corev1.PodSpec, metadata *metav1.ObjectMeta, err error) { - kind := v.policyContext.newResource.GetKind() + newResource := v.policyContext.NewResource() + kind := newResource.GetKind() if kind == "DaemonSet" || kind == "Deployment" || kind == "Job" || kind == "StatefulSet" || kind == "ReplicaSet" || kind == "ReplicationController" { var deployment appsv1.Deployment - resourceBytes, err := v.policyContext.newResource.MarshalJSON() + resourceBytes, err := newResource.MarshalJSON() if err != nil { return nil, nil, err } @@ -499,7 +499,7 @@ func getSpec(v *validator) (podSpec *corev1.PodSpec, metadata *metav1.ObjectMeta } else if kind == "CronJob" { var cronJob batchv1.CronJob - resourceBytes, err := v.policyContext.newResource.MarshalJSON() + resourceBytes, err := newResource.MarshalJSON() if err != nil { return nil, nil, err } @@ -512,7 +512,7 @@ func getSpec(v *validator) (podSpec *corev1.PodSpec, metadata *metav1.ObjectMeta } else if kind == "Pod" { var pod corev1.Pod - resourceBytes, err := v.policyContext.newResource.MarshalJSON() + resourceBytes, err := newResource.MarshalJSON() if err != nil { return nil, nil, err } @@ -566,22 +566,22 @@ func (v *validator) validatePodSecurity() *engineapi.RuleResponse { } func (v *validator) validateResourceWithRule() *engineapi.RuleResponse { - if !isEmptyUnstructured(&v.policyContext.element) { - return v.validatePatterns(v.policyContext.element) + element := v.policyContext.Element() + if !isEmptyUnstructured(&element) { + return v.validatePatterns(element) } - if isDeleteRequest(v.policyContext) { v.log.V(3).Info("skipping validation on deleted resource") return nil } - - resp := v.validatePatterns(v.policyContext.newResource) + resp := v.validatePatterns(v.policyContext.NewResource()) return resp } -func isDeleteRequest(ctx *PolicyContext) bool { +func isDeleteRequest(ctx engineapi.PolicyContext) bool { + newResource := ctx.NewResource() // if the OldResource is not empty, and the NewResource is empty, the request is a DELETE - return isEmptyUnstructured(&ctx.newResource) + return isEmptyUnstructured(&newResource) } func isEmptyUnstructured(u *unstructured.Unstructured) bool { @@ -597,14 +597,14 @@ func isEmptyUnstructured(u *unstructured.Unstructured) bool { } // matches checks if either the new or old resource satisfies the filter conditions defined in the rule -func matches(logger logr.Logger, rule *kyvernov1.Rule, ctx *PolicyContext, subresourceGVKToAPIResource map[string]*metav1.APIResource) bool { - err := MatchesResourceDescription(subresourceGVKToAPIResource, ctx.newResource, *rule, ctx.admissionInfo, ctx.excludeGroupRole, ctx.namespaceLabels, "", ctx.subresource) +func matches(logger logr.Logger, rule *kyvernov1.Rule, ctx engineapi.PolicyContext, subresourceGVKToAPIResource map[string]*metav1.APIResource) bool { + err := MatchesResourceDescription(subresourceGVKToAPIResource, ctx.NewResource(), *rule, ctx.AdmissionInfo(), ctx.ExcludeGroupRole(), ctx.NamespaceLabels(), "", ctx.SubResource()) if err == nil { return true } if !reflect.DeepEqual(ctx.OldResource, unstructured.Unstructured{}) { - err := MatchesResourceDescription(subresourceGVKToAPIResource, ctx.oldResource, *rule, ctx.admissionInfo, ctx.excludeGroupRole, ctx.namespaceLabels, "", ctx.subresource) + err := MatchesResourceDescription(subresourceGVKToAPIResource, ctx.OldResource(), *rule, ctx.AdmissionInfo(), ctx.ExcludeGroupRole(), ctx.NamespaceLabels(), "", ctx.SubResource()) if err == nil { return true } @@ -728,7 +728,7 @@ func (v *validator) buildErrorMessage(err error, path string) string { return fmt.Sprintf("validation error: rule %s execution error: %s", v.rule.Name, err.Error()) } - msgRaw, sErr := variables.SubstituteAll(v.log, v.policyContext.jsonContext, v.rule.Validation.Message) + msgRaw, sErr := variables.SubstituteAll(v.log, v.policyContext.JSONContext(), v.rule.Validation.Message) if sErr != nil { v.log.V(2).Info("failed to substitute variables in message", "error", sErr) return fmt.Sprintf("validation error: variables substitution error in rule %s execution error: %s", v.rule.Name, err.Error()) @@ -759,7 +759,7 @@ func buildAnyPatternErrorMessage(rule *kyvernov1.Rule, errors []string) string { func (v *validator) substitutePatterns() error { if v.pattern != nil { - i, err := variables.SubstituteAll(v.log, v.policyContext.jsonContext, v.pattern) + i, err := variables.SubstituteAll(v.log, v.policyContext.JSONContext(), v.pattern) if err != nil { return err } @@ -769,7 +769,7 @@ func (v *validator) substitutePatterns() error { } if v.anyPattern != nil { - i, err := variables.SubstituteAll(v.log, v.policyContext.jsonContext, v.anyPattern) + i, err := variables.SubstituteAll(v.log, v.policyContext.JSONContext(), v.anyPattern) if err != nil { return err } @@ -785,19 +785,17 @@ func (v *validator) substituteDeny() error { if v.deny == nil { return nil } - - i, err := variables.SubstituteAll(v.log, v.policyContext.jsonContext, v.deny) + i, err := variables.SubstituteAll(v.log, v.policyContext.JSONContext(), v.deny) if err != nil { return err } - v.deny = i.(*kyvernov1.Deny) return nil } // matchesException checks if an exception applies to the resource being admitted func matchesException( - policyContext *PolicyContext, + policyContext engineapi.PolicyContext, rule *kyvernov1.Rule, subresourceGVKToAPIResource map[string]*metav1.APIResource, ) (*kyvernov2alpha1.PolicyException, error) { @@ -807,13 +805,13 @@ func matchesException( } for _, candidate := range candidates { err := matched.CheckMatchesResources( - policyContext.newResource, + policyContext.NewResource(), candidate.Spec.Match, - policyContext.namespaceLabels, + policyContext.NamespaceLabels(), subresourceGVKToAPIResource, - policyContext.subresource, - policyContext.admissionInfo, - policyContext.excludeGroupRole, + policyContext.SubResource(), + policyContext.AdmissionInfo(), + policyContext.ExcludeGroupRole(), ) // if there's no error it means a match if err == nil { @@ -825,7 +823,7 @@ func matchesException( // hasPolicyExceptions returns nil when there are no matching exceptions. // A rule response is returned when an exception is matched, or there is an error. -func hasPolicyExceptions(ctx *PolicyContext, rule *kyvernov1.Rule, subresourceGVKToAPIResource map[string]*metav1.APIResource, log logr.Logger) *engineapi.RuleResponse { +func hasPolicyExceptions(ctx engineapi.PolicyContext, rule *kyvernov1.Rule, subresourceGVKToAPIResource map[string]*metav1.APIResource, log logr.Logger) *engineapi.RuleResponse { // if matches, check if there is a corresponding policy exception exception, err := matchesException(ctx, rule, subresourceGVKToAPIResource) // if we found an exception