1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-31 03:45:17 +00:00

refactor: implement mutation rule handlers (#6684)

* refactor: implement mutation rule handlers

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

---------

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
This commit is contained in:
Charles-Edouard Brétéché 2023-03-27 10:09:46 +02:00 committed by GitHub
parent 873ea5c564
commit e3b4a84c66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 588 additions and 396 deletions

View file

@ -10,6 +10,7 @@ import (
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/internal"
"github.com/kyverno/kyverno/pkg/engine/utils"
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
"github.com/kyverno/kyverno/pkg/engine/variables"
)
@ -84,10 +85,10 @@ func (e *engine) filterRule(
policy := policyContext.Policy()
gvk, subresource := policyContext.ResourceKind()
if err := matchesResourceDescription(newResource, rule, admissionInfo, excludeGroupRole, namespaceLabels, policy.GetNamespace(), gvk, subresource); err != nil {
if err := engineutils.MatchesResourceDescription(newResource, rule, admissionInfo, excludeGroupRole, namespaceLabels, policy.GetNamespace(), gvk, subresource); err != nil {
if ruleType == engineapi.Generation {
// if the oldResource matched, return "false" to delete GR for it
if err = matchesResourceDescription(oldResource, rule, admissionInfo, excludeGroupRole, namespaceLabels, policy.GetNamespace(), gvk, subresource); err == nil {
if err = engineutils.MatchesResourceDescription(oldResource, rule, admissionInfo, excludeGroupRole, namespaceLabels, policy.GetNamespace(), gvk, subresource); err == nil {
return &engineapi.RuleResponse{
Name: rule.Name,
Type: ruleType,

View file

@ -11,18 +11,21 @@ import (
enginecontext "github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/handlers"
"github.com/kyverno/kyverno/pkg/engine/handlers/manifest"
"github.com/kyverno/kyverno/pkg/engine/handlers/mutation"
"github.com/kyverno/kyverno/pkg/engine/internal"
"github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/registryclient"
)
type engine struct {
configuration config.Configuration
client dclient.Interface
rclient registryclient.Client
contextLoader engineapi.ContextLoaderFactory
exceptionSelector engineapi.PolicyExceptionSelector
manifestHandler handlers.Handler
configuration config.Configuration
client dclient.Interface
rclient registryclient.Client
contextLoader engineapi.ContextLoaderFactory
exceptionSelector engineapi.PolicyExceptionSelector
verifyManifestHandler handlers.Handler
mutateHandler handlers.Handler
mutateExistingHandler handlers.Handler
}
func NewEngine(
@ -32,14 +35,17 @@ func NewEngine(
contextLoader engineapi.ContextLoaderFactory,
exceptionSelector engineapi.PolicyExceptionSelector,
) engineapi.Engine {
return &engine{
configuration: configuration,
client: client,
rclient: rclient,
contextLoader: contextLoader,
exceptionSelector: exceptionSelector,
manifestHandler: manifest.NewHandler(client),
e := &engine{
configuration: configuration,
client: client,
rclient: rclient,
contextLoader: contextLoader,
exceptionSelector: exceptionSelector,
verifyManifestHandler: manifest.NewHandler(client),
}
e.mutateHandler = mutation.NewHandler(configuration, e.ContextLoader)
e.mutateExistingHandler = mutation.NewMutateExistingHandler(configuration, client, e.ContextLoader)
return e
}
func (e *engine) Validate(

View file

@ -6,6 +6,7 @@ import (
"github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
type Handler interface {
@ -13,6 +14,21 @@ type Handler interface {
context.Context,
logr.Logger,
engineapi.PolicyContext,
unstructured.Unstructured,
kyvernov1.Rule,
) *engineapi.RuleResponse
func(logr.Logger, engineapi.PolicyContext, kyvernov1.Rule) *engineapi.RuleResponse,
) (unstructured.Unstructured, []engineapi.RuleResponse)
}
func RuleResponses(rrs ...*engineapi.RuleResponse) []engineapi.RuleResponse {
var out []engineapi.RuleResponse
for _, rr := range rrs {
if rr != nil {
out = append(out, *rr)
}
}
if len(out) == 0 {
return nil
}
return out
}

View file

@ -48,21 +48,23 @@ func (h handler) Process(
ctx context.Context,
logger logr.Logger,
policyContext engineapi.PolicyContext,
resource unstructured.Unstructured,
rule kyvernov1.Rule,
) *engineapi.RuleResponse {
_ func(logr.Logger, engineapi.PolicyContext, kyvernov1.Rule) *engineapi.RuleResponse,
) (unstructured.Unstructured, []engineapi.RuleResponse) {
if engineutils.IsDeleteRequest(policyContext) {
return nil
return resource, nil
}
verified, reason, err := h.verifyManifest(ctx, logger, policyContext, *rule.Validation.Manifests)
if err != nil {
logger.V(3).Info("verifyManifest return err", "error", err.Error())
return internal.RuleError(&rule, engineapi.Validation, "error occurred during manifest verification", err)
return resource, handlers.RuleResponses(internal.RuleError(&rule, engineapi.Validation, "error occurred during manifest verification", err))
}
logger.V(3).Info("verifyManifest result", "verified", strconv.FormatBool(verified), "reason", reason)
if !verified {
return internal.RuleResponse(rule, engineapi.Validation, reason, engineapi.RuleStatusFail)
return resource, handlers.RuleResponses(internal.RuleResponse(rule, engineapi.Validation, reason, engineapi.RuleStatusFail))
}
return internal.RulePass(&rule, engineapi.Validation, reason)
return resource, handlers.RuleResponses(internal.RulePass(&rule, engineapi.Validation, reason))
}
func (h handler) verifyManifest(

View file

@ -0,0 +1,185 @@
package mutation
import (
"context"
"fmt"
"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/internal"
"github.com/kyverno/kyverno/pkg/engine/mutate"
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
"github.com/kyverno/kyverno/pkg/utils/api"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func mutateResource(rule *kyvernov1.Rule, ctx engineapi.PolicyContext, resource unstructured.Unstructured, logger logr.Logger) *mutate.Response {
preconditionsPassed, err := internal.CheckPreconditions(logger, ctx, rule.GetAnyAllConditions())
if err != nil {
return mutate.NewErrorResponse("failed to evaluate preconditions", err)
}
if !preconditionsPassed {
return mutate.NewResponse(engineapi.RuleStatusSkip, resource, nil, "preconditions not met")
}
return mutate.Mutate(rule, ctx.JSONContext(), resource, logger)
}
type forEachMutator struct {
rule *kyvernov1.Rule
policyContext engineapi.PolicyContext
foreach []kyvernov1.ForEachMutation
resource resourceInfo
nesting int
contextLoader engineapi.EngineContextLoader
log logr.Logger
}
func (f *forEachMutator) mutateForEach(ctx context.Context) *mutate.Response {
var applyCount int
allPatches := make([][]byte, 0)
for _, foreach := range f.foreach {
if err := f.contextLoader(ctx, f.rule.Context, f.policyContext.JSONContext()); err != nil {
f.log.Error(err, "failed to load context")
return mutate.NewErrorResponse("failed to load context", err)
}
preconditionsPassed, err := internal.CheckPreconditions(f.log, f.policyContext, f.rule.GetAnyAllConditions())
if err != nil {
return mutate.NewErrorResponse("failed to evaluate preconditions", err)
}
if !preconditionsPassed {
return mutate.NewResponse(engineapi.RuleStatusSkip, f.resource.unstructured, nil, "preconditions not met")
}
elements, err := engineutils.EvaluateList(foreach.List, f.policyContext.JSONContext())
if err != nil {
msg := fmt.Sprintf("failed to evaluate list %s: %v", foreach.List, err)
return mutate.NewErrorResponse(msg, err)
}
mutateResp := f.mutateElements(ctx, foreach, elements)
if mutateResp.Status == engineapi.RuleStatusError {
return mutate.NewErrorResponse("failed to mutate elements", err)
}
if mutateResp.Status != engineapi.RuleStatusSkip {
applyCount++
if len(mutateResp.Patches) > 0 {
f.resource.unstructured = mutateResp.PatchedResource
allPatches = append(allPatches, mutateResp.Patches...)
}
f.log.Info("mutateResp.PatchedResource", "resource", mutateResp.PatchedResource)
if err := f.policyContext.JSONContext().AddResource(mutateResp.PatchedResource.Object); err != nil {
f.log.Error(err, "failed to update resource in context")
}
}
}
msg := fmt.Sprintf("%d elements processed", applyCount)
if applyCount == 0 {
return mutate.NewResponse(engineapi.RuleStatusSkip, f.resource.unstructured, allPatches, msg)
}
return mutate.NewResponse(engineapi.RuleStatusPass, f.resource.unstructured, allPatches, msg)
}
func (f *forEachMutator) mutateElements(ctx context.Context, foreach kyvernov1.ForEachMutation, elements []interface{}) *mutate.Response {
f.policyContext.JSONContext().Checkpoint()
defer f.policyContext.JSONContext().Restore()
patchedResource := f.resource
var allPatches [][]byte
if foreach.RawPatchStrategicMerge != nil {
engineutils.InvertedElement(elements)
}
for index, element := range elements {
if element == nil {
continue
}
f.policyContext.JSONContext().Reset()
policyContext := f.policyContext.Copy()
falseVar := false
if err := engineutils.AddElementToContext(policyContext, element, index, f.nesting, &falseVar); err != nil {
return mutate.NewErrorResponse(fmt.Sprintf("failed to add element to mutate.foreach[%d].context", index), err)
}
if err := f.contextLoader(ctx, foreach.Context, policyContext.JSONContext()); err != nil {
return mutate.NewErrorResponse(fmt.Sprintf("failed to load to mutate.foreach[%d].context", index), err)
}
preconditionsPassed, err := internal.CheckPreconditions(f.log, policyContext, foreach.AnyAllConditions)
if err != nil {
return mutate.NewErrorResponse(fmt.Sprintf("failed to evaluate mutate.foreach[%d].preconditions", index), err)
}
if !preconditionsPassed {
f.log.Info("mutate.foreach.preconditions not met", "elementIndex", index)
continue
}
var mutateResp *mutate.Response
if foreach.ForEachMutation != nil {
nestedForEach, err := api.DeserializeJSONArray[kyvernov1.ForEachMutation](foreach.ForEachMutation)
if err != nil {
return mutate.NewErrorResponse("failed to deserialize foreach", err)
}
m := &forEachMutator{
rule: f.rule,
policyContext: f.policyContext,
resource: patchedResource,
log: f.log,
foreach: nestedForEach,
nesting: f.nesting + 1,
contextLoader: f.contextLoader,
}
mutateResp = m.mutateForEach(ctx)
} else {
mutateResp = mutate.ForEach(f.rule.Name, foreach, policyContext.JSONContext(), patchedResource.unstructured, f.log)
}
if mutateResp.Status == engineapi.RuleStatusFail || mutateResp.Status == engineapi.RuleStatusError {
return mutateResp
}
if len(mutateResp.Patches) > 0 {
patchedResource.unstructured = mutateResp.PatchedResource
allPatches = append(allPatches, mutateResp.Patches...)
}
}
return mutate.NewResponse(engineapi.RuleStatusPass, patchedResource.unstructured, allPatches, "")
}
func buildRuleResponse(rule *kyvernov1.Rule, mutateResp *mutate.Response, info resourceInfo) *engineapi.RuleResponse {
resp := internal.RuleResponse(*rule, engineapi.Mutation, mutateResp.Message, mutateResp.Status)
if resp.Status == engineapi.RuleStatusPass {
resp.Patches = mutateResp.Patches
resp.Message = buildSuccessMessage(mutateResp.PatchedResource)
}
if len(rule.Mutation.Targets) != 0 {
resp.PatchedTarget = &mutateResp.PatchedResource
resp.PatchedTargetSubresourceName = info.subresource
resp.PatchedTargetParentResourceGVR = info.parentResourceGVR
}
return resp
}
func buildSuccessMessage(r unstructured.Unstructured) string {
if r.Object == nil {
return "mutated resource"
}
if r.GetNamespace() == "" {
return fmt.Sprintf("mutated %s/%s", r.GetKind(), r.GetName())
}
return fmt.Sprintf("mutated %s/%s in namespace %s", r.GetKind(), r.GetName(), r.GetNamespace())
}

View file

@ -1,4 +1,4 @@
package engine
package mutation
import (
"context"

View file

@ -1,4 +1,4 @@
package engine
package mutation
import (
"fmt"

View file

@ -0,0 +1,109 @@
package mutation
import (
"context"
"github.com/go-logr/logr"
gojmespath "github.com/jmespath/go-jmespath"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/config"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/handlers"
"github.com/kyverno/kyverno/pkg/engine/mutate"
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
type handler struct {
configuration config.Configuration
contextLoader func(kyvernov1.PolicyInterface, kyvernov1.Rule) engineapi.EngineContextLoader
}
func NewHandler(
configuration config.Configuration,
contextLoader func(kyvernov1.PolicyInterface, kyvernov1.Rule) engineapi.EngineContextLoader,
) handlers.Handler {
return handler{
configuration: configuration,
contextLoader: contextLoader,
}
}
func (h handler) Process(
ctx context.Context,
logger logr.Logger,
policyContext engineapi.PolicyContext,
resource unstructured.Unstructured,
rule kyvernov1.Rule,
polexFilter func(logr.Logger, engineapi.PolicyContext, kyvernov1.Rule) *engineapi.RuleResponse,
) (unstructured.Unstructured, []engineapi.RuleResponse) {
policy := policyContext.Policy()
contextLoader := h.contextLoader(policy, rule)
var excludeResource []string
if len(h.configuration.GetExcludedGroups()) > 0 {
excludeResource = h.configuration.GetExcludedGroups()
}
gvk, subresource := policyContext.ResourceKind()
if err := engineutils.MatchesResourceDescription(
resource,
rule,
policyContext.AdmissionInfo(),
excludeResource,
policyContext.NamespaceLabels(),
policyContext.Policy().GetNamespace(),
gvk,
subresource,
); err != nil {
logger.V(4).Info("rule not matched", "reason", err.Error())
return resource, nil
}
// check if there is a corresponding policy exception
if ruleResp := polexFilter(logger, policyContext, rule); ruleResp != nil {
return resource, handlers.RuleResponses(ruleResp)
}
logger.V(3).Info("processing mutate rule")
if err := contextLoader(ctx, rule.Context, policyContext.JSONContext()); err != nil {
if _, ok := err.(gojmespath.NotFoundError); ok {
logger.V(3).Info("failed to load context", "reason", err.Error())
} else {
logger.Error(err, "failed to load context")
}
// TODO: return error ?
return resource, nil
}
var parentResourceGVR metav1.GroupVersionResource
if subresource != "" {
parentResourceGVR = policyContext.RequestResource()
}
resourceInfo := resourceInfo{
unstructured: resource,
subresource: subresource,
parentResourceGVR: parentResourceGVR,
}
// logger.V(4).Info("apply rule to resource", "resource namespace", patchedResource.unstructured.GetNamespace(), "resource name", patchedResource.unstructured.GetName())
var mutateResp *mutate.Response
if rule.Mutation.ForEachMutation != nil {
m := &forEachMutator{
rule: &rule,
foreach: rule.Mutation.ForEachMutation,
policyContext: policyContext,
resource: resourceInfo,
log: logger,
contextLoader: contextLoader,
nesting: 0,
}
mutateResp = m.mutateForEach(ctx)
} else {
mutateResp = mutateResource(&rule, policyContext, resourceInfo.unstructured, logger)
}
if mutateResp == nil {
return resource, nil
}
return mutateResp.PatchedResource, handlers.RuleResponses(buildRuleResponse(&rule, mutateResp, resourceInfo))
}

View file

@ -0,0 +1,124 @@
package mutation
import (
"context"
"github.com/go-logr/logr"
gojmespath "github.com/jmespath/go-jmespath"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/clients/dclient"
"github.com/kyverno/kyverno/pkg/config"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/handlers"
"github.com/kyverno/kyverno/pkg/engine/internal"
"github.com/kyverno/kyverno/pkg/engine/mutate"
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
type handlerExisting struct {
configuration config.Configuration
client dclient.Interface
contextLoader func(kyvernov1.PolicyInterface, kyvernov1.Rule) engineapi.EngineContextLoader
}
func NewMutateExistingHandler(
configuration config.Configuration,
client dclient.Interface,
contextLoader func(kyvernov1.PolicyInterface, kyvernov1.Rule) engineapi.EngineContextLoader,
) handlers.Handler {
return handlerExisting{
configuration: configuration,
client: client,
contextLoader: contextLoader,
}
}
func (h handlerExisting) Process(
ctx context.Context,
logger logr.Logger,
policyContext engineapi.PolicyContext,
resource unstructured.Unstructured,
rule kyvernov1.Rule,
polexFilter func(logr.Logger, engineapi.PolicyContext, kyvernov1.Rule) *engineapi.RuleResponse,
) (unstructured.Unstructured, []engineapi.RuleResponse) {
policy := policyContext.Policy()
contextLoader := h.contextLoader(policy, rule)
var responses []engineapi.RuleResponse
var excludeResource []string
if len(h.configuration.GetExcludedGroups()) > 0 {
excludeResource = h.configuration.GetExcludedGroups()
}
gvk, subresource := policyContext.ResourceKind()
if err := engineutils.MatchesResourceDescription(
resource,
rule,
policyContext.AdmissionInfo(),
excludeResource,
policyContext.NamespaceLabels(),
policyContext.Policy().GetNamespace(),
gvk,
subresource,
); err != nil {
logger.V(4).Info("rule not matched", "reason", err.Error())
return resource, nil
}
// check if there is a corresponding policy exception
if ruleResp := polexFilter(logger, policyContext, rule); ruleResp != nil {
return resource, handlers.RuleResponses(ruleResp)
}
logger.V(3).Info("processing mutate rule")
if err := contextLoader(ctx, rule.Context, policyContext.JSONContext()); err != nil {
if _, ok := err.(gojmespath.NotFoundError); ok {
logger.V(3).Info("failed to load context", "reason", err.Error())
} else {
logger.Error(err, "failed to load context")
}
// TODO: return error ?
return resource, nil
}
var patchedResources []resourceInfo
targets, err := loadTargets(h.client, rule.Mutation.Targets, policyContext, logger)
if err != nil {
rr := internal.RuleError(&rule, engineapi.Mutation, "", err)
responses = append(responses, *rr)
} else {
patchedResources = append(patchedResources, targets...)
}
for _, patchedResource := range patchedResources {
if patchedResource.unstructured.Object == nil {
continue
}
policyContext := policyContext.Copy()
if err := policyContext.JSONContext().AddTargetResource(patchedResource.unstructured.Object); err != nil {
logger.Error(err, "failed to add target resource to the context")
continue
}
// logger.V(4).Info("apply rule to resource", "resource namespace", patchedResource.unstructured.GetNamespace(), "resource name", patchedResource.unstructured.GetName())
var mutateResp *mutate.Response
if rule.Mutation.ForEachMutation != nil {
m := &forEachMutator{
rule: &rule,
foreach: rule.Mutation.ForEachMutation,
policyContext: policyContext,
resource: patchedResource,
log: logger,
contextLoader: contextLoader,
nesting: 0,
}
mutateResp = m.mutateForEach(ctx)
} else {
mutateResp = mutateResource(&rule, policyContext, patchedResource.unstructured, logger)
}
if ruleResponse := buildRuleResponse(&rule, mutateResp, patchedResource); ruleResponse != nil {
responses = append(responses, *ruleResponse)
}
}
return resource, responses
}

View file

@ -6,16 +6,12 @@ import (
"time"
"github.com/go-logr/logr"
gojmespath "github.com/jmespath/go-jmespath"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/autogen"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/internal"
"github.com/kyverno/kyverno/pkg/engine/mutate"
"github.com/kyverno/kyverno/pkg/tracing"
"github.com/kyverno/kyverno/pkg/utils/api"
"go.opentelemetry.io/otel/trace"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@ -25,326 +21,53 @@ func (e *engine) mutate(
logger logr.Logger,
policyContext engineapi.PolicyContext,
) engineapi.EngineResponse {
policy := policyContext.Policy()
resp := engineapi.NewEngineResponseFromPolicyContext(policyContext, nil)
startTime := time.Now()
resp := engineapi.NewEngineResponseFromPolicyContext(policyContext, nil)
policy := policyContext.Policy()
matchedResource := policyContext.NewResource()
var skippedRules []string
logger.V(4).Info("start mutate policy processing", "startTime", startTime)
startMutateResultResponse(&resp, policy, matchedResource)
policyContext.JSONContext().Checkpoint()
defer policyContext.JSONContext().Restore()
var err error
applyRules := policy.GetSpec().GetApplyRules()
computeRules := autogen.ComputeRules(policy)
for i, rule := range computeRules {
for _, rule := range autogen.ComputeRules(policy) {
if !rule.HasMutate() {
continue
}
tracing.ChildSpan(
resource, ruleResp := tracing.ChildSpan2(
ctx,
"pkg/engine",
fmt.Sprintf("RULE %s", rule.Name),
func(ctx context.Context, span trace.Span) {
func(ctx context.Context, span trace.Span) (unstructured.Unstructured, []engineapi.RuleResponse) {
logger := internal.LoggerWithRule(logger, rule)
var excludeResource []string
if len(e.configuration.GetExcludedGroups()) > 0 {
excludeResource = e.configuration.GetExcludedGroups()
polexFilter := func(logger logr.Logger, policyContext engineapi.PolicyContext, rule kyvernov1.Rule) *engineapi.RuleResponse {
return hasPolicyExceptions(logger, engineapi.Validation, e.exceptionSelector, policyContext, &rule, e.configuration)
}
gvk, subresource := policyContext.ResourceKind()
if err = matchesResourceDescription(
matchedResource,
rule,
policyContext.AdmissionInfo(),
excludeResource,
policyContext.NamespaceLabels(),
policyContext.Policy().GetNamespace(),
gvk,
subresource,
); err != nil {
logger.V(4).Info("rule not matched", "reason", err.Error())
skippedRules = append(skippedRules, rule.Name)
return
}
// check if there is a corresponding policy exception
if ruleResp := hasPolicyExceptions(logger, engineapi.Mutation, e.exceptionSelector, policyContext, &computeRules[i], e.configuration); ruleResp != nil {
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp)
return
}
logger.V(3).Info("processing mutate rule")
if err := internal.LoadContext(ctx, e, policyContext, rule); err != nil {
if _, ok := err.(gojmespath.NotFoundError); ok {
logger.V(3).Info("failed to load context", "reason", err.Error())
} else {
logger.Error(err, "failed to load context")
}
return
}
ruleCopy := rule.DeepCopy()
var patchedResources []resourceInfo
if !policyContext.AdmissionOperation() && rule.IsMutateExisting() {
targets, err := loadTargets(e.client, ruleCopy.Mutation.Targets, policyContext, logger)
if err != nil {
rr := internal.RuleError(ruleCopy, engineapi.Mutation, "", err)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *rr)
} else {
patchedResources = append(patchedResources, targets...)
}
return e.mutateExistingHandler.Process(ctx, logger, policyContext, matchedResource, rule, polexFilter)
} else {
var parentResourceGVR metav1.GroupVersionResource
if subresource != "" {
parentResourceGVR = policyContext.RequestResource()
}
patchedResources = append(patchedResources, resourceInfo{
unstructured: matchedResource,
subresource: subresource,
parentResourceGVR: parentResourceGVR,
})
}
for _, patchedResource := range patchedResources {
if patchedResource.unstructured.Object == nil {
continue
}
if !policyContext.AdmissionOperation() && rule.IsMutateExisting() {
policyContext := policyContext.Copy()
if err := policyContext.JSONContext().AddTargetResource(patchedResource.unstructured.Object); err != nil {
logger.Error(err, "failed to add target resource to the context")
continue
}
}
logger.V(4).Info("apply rule to resource", "resource namespace", patchedResource.unstructured.GetNamespace(), "resource name", patchedResource.unstructured.GetName())
var mutateResp *mutate.Response
if rule.Mutation.ForEachMutation != nil {
m := &forEachMutator{
rule: ruleCopy,
foreach: rule.Mutation.ForEachMutation,
policyContext: policyContext,
resource: patchedResource,
log: logger,
contextLoader: e.ContextLoader(policyContext.Policy(), *ruleCopy),
nesting: 0,
}
mutateResp = m.mutateForEach(ctx)
} else {
mutateResp = mutateResource(ruleCopy, policyContext, patchedResource.unstructured, logger)
}
matchedResource = mutateResp.PatchedResource
if ruleResponse := buildRuleResponse(ruleCopy, mutateResp, patchedResource); ruleResponse != nil {
internal.AddRuleResponse(&resp.PolicyResponse, ruleResponse, startTime)
}
return e.mutateHandler.Process(ctx, logger, policyContext, matchedResource, rule, polexFilter)
}
},
)
matchedResource = resource
for _, ruleResp := range ruleResp {
ruleResp := ruleResp
internal.AddRuleResponse(&resp.PolicyResponse, &ruleResp, startTime)
logger.V(4).Info("finished processing rule", "processingTime", ruleResp.Stats.ProcessingTime.String())
}
if applyRules == kyvernov1.ApplyOne && resp.PolicyResponse.Stats.RulesAppliedCount > 0 {
break
}
}
for _, r := range resp.PolicyResponse.Rules {
for _, n := range skippedRules {
if r.Name == n {
r.Status = engineapi.RuleStatusSkip
logger.V(4).Info("rule Status set as skip", "rule skippedRules", r.Name)
}
}
}
resp.PatchedResource = matchedResource
endMutateResultResponse(logger, &resp, startTime)
return resp
}
func mutateResource(rule *kyvernov1.Rule, ctx engineapi.PolicyContext, resource unstructured.Unstructured, logger logr.Logger) *mutate.Response {
preconditionsPassed, err := internal.CheckPreconditions(logger, ctx, rule.GetAnyAllConditions())
if err != nil {
return mutate.NewErrorResponse("failed to evaluate preconditions", err)
}
if !preconditionsPassed {
return mutate.NewResponse(engineapi.RuleStatusSkip, resource, nil, "preconditions not met")
}
return mutate.Mutate(rule, ctx.JSONContext(), resource, logger)
}
type forEachMutator struct {
rule *kyvernov1.Rule
policyContext engineapi.PolicyContext
foreach []kyvernov1.ForEachMutation
resource resourceInfo
nesting int
contextLoader engineapi.EngineContextLoader
log logr.Logger
}
func (f *forEachMutator) mutateForEach(ctx context.Context) *mutate.Response {
var applyCount int
allPatches := make([][]byte, 0)
for _, foreach := range f.foreach {
if err := f.contextLoader(ctx, f.rule.Context, f.policyContext.JSONContext()); err != nil {
f.log.Error(err, "failed to load context")
return mutate.NewErrorResponse("failed to load context", err)
}
preconditionsPassed, err := internal.CheckPreconditions(f.log, f.policyContext, f.rule.GetAnyAllConditions())
if err != nil {
return mutate.NewErrorResponse("failed to evaluate preconditions", err)
}
if !preconditionsPassed {
return mutate.NewResponse(engineapi.RuleStatusSkip, f.resource.unstructured, nil, "preconditions not met")
}
elements, err := evaluateList(foreach.List, f.policyContext.JSONContext())
if err != nil {
msg := fmt.Sprintf("failed to evaluate list %s: %v", foreach.List, err)
return mutate.NewErrorResponse(msg, err)
}
mutateResp := f.mutateElements(ctx, foreach, elements)
if mutateResp.Status == engineapi.RuleStatusError {
return mutate.NewErrorResponse("failed to mutate elements", err)
}
if mutateResp.Status != engineapi.RuleStatusSkip {
applyCount++
if len(mutateResp.Patches) > 0 {
f.resource.unstructured = mutateResp.PatchedResource
allPatches = append(allPatches, mutateResp.Patches...)
}
f.log.Info("mutateResp.PatchedResource", "resource", mutateResp.PatchedResource)
if err := f.policyContext.JSONContext().AddResource(mutateResp.PatchedResource.Object); err != nil {
f.log.Error(err, "failed to update resource in context")
}
}
}
msg := fmt.Sprintf("%d elements processed", applyCount)
if applyCount == 0 {
return mutate.NewResponse(engineapi.RuleStatusSkip, f.resource.unstructured, allPatches, msg)
}
return mutate.NewResponse(engineapi.RuleStatusPass, f.resource.unstructured, allPatches, msg)
}
func (f *forEachMutator) mutateElements(ctx context.Context, foreach kyvernov1.ForEachMutation, elements []interface{}) *mutate.Response {
f.policyContext.JSONContext().Checkpoint()
defer f.policyContext.JSONContext().Restore()
patchedResource := f.resource
var allPatches [][]byte
if foreach.RawPatchStrategicMerge != nil {
invertedElement(elements)
}
for index, element := range elements {
if element == nil {
continue
}
f.policyContext.JSONContext().Reset()
policyContext := f.policyContext.Copy()
falseVar := false
if err := addElementToContext(policyContext, element, index, f.nesting, &falseVar); err != nil {
return mutate.NewErrorResponse(fmt.Sprintf("failed to add element to mutate.foreach[%d].context", index), err)
}
if err := f.contextLoader(ctx, foreach.Context, policyContext.JSONContext()); err != nil {
return mutate.NewErrorResponse(fmt.Sprintf("failed to load to mutate.foreach[%d].context", index), err)
}
preconditionsPassed, err := internal.CheckPreconditions(f.log, policyContext, foreach.AnyAllConditions)
if err != nil {
return mutate.NewErrorResponse(fmt.Sprintf("failed to evaluate mutate.foreach[%d].preconditions", index), err)
}
if !preconditionsPassed {
f.log.Info("mutate.foreach.preconditions not met", "elementIndex", index)
continue
}
var mutateResp *mutate.Response
if foreach.ForEachMutation != nil {
nestedForEach, err := api.DeserializeJSONArray[kyvernov1.ForEachMutation](foreach.ForEachMutation)
if err != nil {
return mutate.NewErrorResponse("failed to deserialize foreach", err)
}
m := &forEachMutator{
rule: f.rule,
policyContext: f.policyContext,
resource: patchedResource,
log: f.log,
foreach: nestedForEach,
nesting: f.nesting + 1,
contextLoader: f.contextLoader,
}
mutateResp = m.mutateForEach(ctx)
} else {
mutateResp = mutate.ForEach(f.rule.Name, foreach, policyContext.JSONContext(), patchedResource.unstructured, f.log)
}
if mutateResp.Status == engineapi.RuleStatusFail || mutateResp.Status == engineapi.RuleStatusError {
return mutateResp
}
if len(mutateResp.Patches) > 0 {
patchedResource.unstructured = mutateResp.PatchedResource
allPatches = append(allPatches, mutateResp.Patches...)
}
}
return mutate.NewResponse(engineapi.RuleStatusPass, patchedResource.unstructured, allPatches, "")
}
func buildRuleResponse(rule *kyvernov1.Rule, mutateResp *mutate.Response, info resourceInfo) *engineapi.RuleResponse {
resp := internal.RuleResponse(*rule, engineapi.Mutation, mutateResp.Message, mutateResp.Status)
if resp.Status == engineapi.RuleStatusPass {
resp.Patches = mutateResp.Patches
resp.Message = buildSuccessMessage(mutateResp.PatchedResource)
}
if len(rule.Mutation.Targets) != 0 {
resp.PatchedTarget = &mutateResp.PatchedResource
resp.PatchedTargetSubresourceName = info.subresource
resp.PatchedTargetParentResourceGVR = info.parentResourceGVR
}
return resp
}
func buildSuccessMessage(r unstructured.Unstructured) string {
if r.Object == nil {
return "mutated resource"
}
if r.GetNamespace() == "" {
return fmt.Sprintf("mutated %s/%s", r.GetKind(), r.GetName())
}
return fmt.Sprintf("mutated %s/%s in namespace %s", r.GetKind(), r.GetName(), r.GetNamespace())
}
func startMutateResultResponse(resp *engineapi.EngineResponse, policy kyvernov1.PolicyInterface, resource unstructured.Unstructured) {
if resp == nil {
return

View file

@ -59,6 +59,13 @@ func (c *PolicyContext) Policy() kyvernov1.PolicyInterface {
}
func (c *PolicyContext) NewResource() unstructured.Unstructured {
// object, err := c.jsonContext.Query("request.object")
// if err == nil {
// if o, ok := object.(map[string]interface{}); ok {
// return unstructured.Unstructured{Object: o}
// }
// }
// return unstructured.Unstructured{}
return c.newResource
}

View file

@ -0,0 +1,63 @@
package utils
import (
"fmt"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
enginecontext "github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/variables"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func EvaluateList(jmesPath string, ctx enginecontext.EvalInterface) ([]interface{}, error) {
i, err := ctx.Query(jmesPath)
if err != nil {
return nil, err
}
l, ok := i.([]interface{})
if !ok {
return []interface{}{i}, nil
}
return l, nil
}
// InvertedElement inverted the order of element for patchStrategicMerge policies as kustomize patch revering the order of patch resources.
func InvertedElement(elements []interface{}) {
for i, j := 0, len(elements)-1; i < j; i, j = i+1, j-1 {
elements[i], elements[j] = elements[j], elements[i]
}
}
func AddElementToContext(ctx engineapi.PolicyContext, element interface{}, index, nesting int, elementScope *bool) error {
data, err := variables.DocumentToUntyped(element)
if err != nil {
return err
}
if err := ctx.JSONContext().AddElement(data, index, nesting); err != nil {
return fmt.Errorf("failed to add element (%v) to JSON context: %w", element, err)
}
dataMap, ok := data.(map[string]interface{})
// We set scoped to true by default if the data is a map
// otherwise we do not do element scoped foreach unless the user
// has explicitly set it to true
scoped := ok
// If the user has explicitly provided an element scope
// we check if data is a map or not. In case it is not a map and the user
// has set elementscoped to true, we throw an error.
// Otherwise we set the value to what is specified by the user.
if elementScope != nil {
if *elementScope && !ok {
return fmt.Errorf("cannot use elementScope=true foreach rules for elements that are not maps, expected type=map got type=%T", data)
}
scoped = *elementScope
}
if scoped {
u := unstructured.Unstructured{}
u.SetUnstructuredContent(dataMap)
ctx.SetElement(u)
}
return nil
}

View file

@ -1,4 +1,4 @@
package engine
package utils
import (
"fmt"
@ -6,7 +6,6 @@ import (
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
"github.com/kyverno/kyverno/pkg/engine/context"
datautils "github.com/kyverno/kyverno/pkg/utils/data"
matchutils "github.com/kyverno/kyverno/pkg/utils/match"
"github.com/kyverno/kyverno/pkg/utils/wildcard"
@ -155,7 +154,7 @@ func matchSubjects(ruleSubjects []rbacv1.Subject, userInfo authenticationv1.User
}
// matchesResourceDescription checks if the resource matches resource description of the rule or not
func matchesResourceDescription(
func MatchesResourceDescription(
resourceRef unstructured.Unstructured,
ruleRef kyvernov1.Rule,
admissionInfoRef kyvernov1beta1.RequestInfo,
@ -326,24 +325,3 @@ func ManagedPodResource(policy kyvernov1.PolicyInterface, resource unstructured.
return false
}
func evaluateList(jmesPath string, ctx context.EvalInterface) ([]interface{}, error) {
i, err := ctx.Query(jmesPath)
if err != nil {
return nil, err
}
l, ok := i.([]interface{})
if !ok {
return []interface{}{i}, nil
}
return l, nil
}
// invertedElement inverted the order of element for patchStrategicMerge policies as kustomize patch revering the order of patch resources.
func invertedElement(elements []interface{}) {
for i, j := 0, len(elements)-1; i < j; i, j = i+1, j-1 {
elements[i], elements[j] = elements[j], elements[i]
}
}

View file

@ -1,4 +1,4 @@
package engine
package utils
import (
"encoding/json"
@ -906,7 +906,7 @@ func TestMatchesResourceDescription(t *testing.T) {
resource, _ := kubeutils.BytesToUnstructured(tc.Resource)
for _, rule := range autogen.ComputeRules(&policy) {
err := matchesResourceDescription(*resource, rule, tc.AdmissionInfo, []string{}, nil, "", resource.GroupVersionKind(), "")
err := MatchesResourceDescription(*resource, rule, tc.AdmissionInfo, []string{}, nil, "", resource.GroupVersionKind(), "")
if err != nil {
if !tc.areErrorsExpected {
t.Errorf("Testcase %d Unexpected error: %v\nmsg: %s", i+1, err, tc.Description)
@ -1811,7 +1811,7 @@ func TestMatchesResourceDescription_GenerateName(t *testing.T) {
resource, _ := kubeutils.BytesToUnstructured(tc.Resource)
for _, rule := range autogen.ComputeRules(&policy) {
err := matchesResourceDescription(*resource, rule, tc.AdmissionInfo, []string{}, nil, "", resource.GroupVersionKind(), "")
err := MatchesResourceDescription(*resource, rule, tc.AdmissionInfo, []string{}, nil, "", resource.GroupVersionKind(), "")
if err != nil {
if !tc.areErrorsExpected {
t.Errorf("Testcase %d Unexpected error: %v\nmsg: %s", i+1, err, tc.Description)
@ -1878,7 +1878,7 @@ func TestResourceDescriptionMatch_MultipleKind(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := matchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
@ -1990,7 +1990,7 @@ func TestResourceDescriptionMatch_ExcludeDefaultGroups(t *testing.T) {
}
// First test: confirm that this above rule produces errors (and raise an error if err == nil)
if err := matchesResourceDescription(*resource, rule, requestInfo, dynamicConfig, nil, "", resource.GroupVersionKind(), ""); err == nil {
if err := MatchesResourceDescription(*resource, rule, requestInfo, dynamicConfig, nil, "", resource.GroupVersionKind(), ""); err == nil {
t.Error("Testcase was expected to fail, but err was nil")
}
@ -2010,7 +2010,7 @@ func TestResourceDescriptionMatch_ExcludeDefaultGroups(t *testing.T) {
}
// Second test: confirm that matching this rule does not create any errors (and raise if err != nil)
if err := matchesResourceDescription(*resource, rule2, requestInfo, dynamicConfig, nil, "", resource.GroupVersionKind(), ""); err != nil {
if err := MatchesResourceDescription(*resource, rule2, requestInfo, dynamicConfig, nil, "", resource.GroupVersionKind(), ""); err != nil {
t.Errorf("Testcase was expected to not fail, but err was %s", err)
}
@ -2024,7 +2024,7 @@ func TestResourceDescriptionMatch_ExcludeDefaultGroups(t *testing.T) {
}}
// Third test: confirm that now the custom exclude-snippet should run in CheckSubjects() and that should result in this rule failing (raise if err == nil for that reason)
if err := matchesResourceDescription(*resource, rule2, requestInfo, dynamicConfig, nil, "", resource.GroupVersionKind(), ""); err == nil {
if err := MatchesResourceDescription(*resource, rule2, requestInfo, dynamicConfig, nil, "", resource.GroupVersionKind(), ""); err == nil {
t.Error("Testcase was expected to fail, but err was nil #1!")
}
}
@ -2083,7 +2083,7 @@ func TestResourceDescriptionMatch_Name(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := matchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
@ -2141,7 +2141,7 @@ func TestResourceDescriptionMatch_GenerateName(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := matchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
@ -2200,7 +2200,7 @@ func TestResourceDescriptionMatch_Name_Regex(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := matchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
@ -2258,7 +2258,7 @@ func TestResourceDescriptionMatch_GenerateName_Regex(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := matchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
@ -2325,7 +2325,7 @@ func TestResourceDescriptionMatch_Label_Expression_NotMatch(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := matchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
@ -2393,7 +2393,7 @@ func TestResourceDescriptionMatch_Label_Expression_Match(t *testing.T) {
}
rule := v1.Rule{MatchResources: v1.MatchResources{ResourceDescription: resourceDescription}}
if err := matchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
@ -2474,7 +2474,7 @@ func TestResourceDescriptionExclude_Label_Expression_Match(t *testing.T) {
ExcludeResources: v1.MatchResources{ResourceDescription: resourceDescriptionExclude},
}
if err := matchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err == nil {
if err := MatchesResourceDescription(*resource, rule, v1beta1.RequestInfo{}, []string{}, nil, "", resource.GroupVersionKind(), ""); err == nil {
t.Errorf("Testcase has failed due to the following:\n Function has returned no error, even though it was supposed to fail")
}
}

View file

@ -13,6 +13,7 @@ import (
"github.com/kyverno/kyverno/pkg/autogen"
"github.com/kyverno/kyverno/pkg/config"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/handlers"
"github.com/kyverno/kyverno/pkg/engine/internal"
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
"github.com/kyverno/kyverno/pkg/engine/validate"
@ -68,7 +69,7 @@ func (e *engine) validateResource(
ctx,
"pkg/engine",
fmt.Sprintf("RULE %s", rule.Name),
func(ctx context.Context, span trace.Span) *engineapi.RuleResponse {
func(ctx context.Context, span trace.Span) []engineapi.RuleResponse {
hasValidate := rule.HasValidate()
hasValidateImage := rule.HasImagesValidationChecks()
hasYAMLSignatureVerify := rule.HasYAMLSignatureVerify()
@ -81,21 +82,30 @@ func (e *engine) validateResource(
// check if there is a corresponding policy exception
ruleResp := hasPolicyExceptions(logger, engineapi.Validation, e.exceptionSelector, policyContext, rule, e.configuration)
if ruleResp != nil {
return ruleResp
return handlers.RuleResponses(ruleResp)
}
policyContext.JSONContext().Reset()
if hasValidate && !hasYAMLSignatureVerify {
return e.processValidationRule(ctx, logger, policyContext, rule)
return handlers.RuleResponses(e.processValidationRule(ctx, logger, policyContext, rule))
} else if hasValidateImage {
return e.processImageValidationRule(ctx, logger, policyContext, rule)
return handlers.RuleResponses(e.processImageValidationRule(ctx, logger, policyContext, rule))
} else if hasYAMLSignatureVerify {
return e.manifestHandler.Process(ctx, logger, policyContext, *rule)
_, rr := e.verifyManifestHandler.Process(
ctx,
logger,
policyContext,
policyContext.NewResource(),
*rule,
nil,
)
return rr
}
return nil
},
)
if ruleResp != nil {
internal.AddRuleResponse(resp, ruleResp, startTime)
for _, ruleResp := range ruleResp {
ruleResp := ruleResp
internal.AddRuleResponse(resp, &ruleResp, startTime)
logger.V(4).Info("finished processing rule", "processingTime", ruleResp.Stats.ProcessingTime.String())
}
if applyRules == kyvernov1.ApplyOne && resp.Stats.RulesAppliedCount > 0 {
@ -228,7 +238,7 @@ func (v *validator) validate(ctx context.Context) *engineapi.RuleResponse {
func (v *validator) validateForEach(ctx context.Context) *engineapi.RuleResponse {
applyCount := 0
for _, foreach := range v.forEach {
elements, err := evaluateList(foreach.List, v.policyContext.JSONContext())
elements, err := engineutils.EvaluateList(foreach.List, v.policyContext.JSONContext())
if err != nil {
v.log.V(2).Info("failed to evaluate list", "list", foreach.List, "error", err.Error())
continue
@ -260,7 +270,7 @@ func (v *validator) validateElements(ctx context.Context, foreach kyvernov1.ForE
v.policyContext.JSONContext().Reset()
policyContext := v.policyContext.Copy()
if err := addElementToContext(policyContext, element, index, v.nesting, elementScope); err != nil {
if err := engineutils.AddElementToContext(policyContext, element, index, v.nesting, elementScope); err != nil {
v.log.Error(err, "failed to add element to context")
return internal.RuleError(v.rule, engineapi.Validation, "failed to process foreach", err), applyCount
}
@ -296,38 +306,6 @@ func (v *validator) validateElements(ctx context.Context, foreach kyvernov1.ForE
return internal.RulePass(v.rule, engineapi.Validation, ""), applyCount
}
func addElementToContext(ctx engineapi.PolicyContext, element interface{}, index, nesting int, elementScope *bool) error {
data, err := variables.DocumentToUntyped(element)
if err != nil {
return err
}
if err := ctx.JSONContext().AddElement(data, index, nesting); err != nil {
return fmt.Errorf("failed to add element (%v) to JSON context: %w", element, err)
}
dataMap, ok := data.(map[string]interface{})
// We set scoped to true by default if the data is a map
// otherwise we do not do element scoped foreach unless the user
// has explicitly set it to true
scoped := ok
// If the user has explicitly provided an element scope
// we check if data is a map or not. In case it is not a map and the user
// has set elementscoped to true, we throw an error.
// Otherwise we set the value to what is specified by the user.
if elementScope != nil {
if *elementScope && !ok {
return fmt.Errorf("cannot use elementScope=true foreach rules for elements that are not maps, expected type=map got type=%T", data)
}
scoped = *elementScope
}
if scoped {
u := unstructured.Unstructured{}
u.SetUnstructuredContent(dataMap)
ctx.SetElement(u)
}
return nil
}
func (v *validator) loadContext(ctx context.Context) error {
if err := v.contextLoader(ctx, v.contextEntries, v.policyContext.JSONContext()); err != nil {
if _, ok := err.(gojmespath.NotFoundError); ok {
@ -481,7 +459,7 @@ func matches(
cfg config.Configuration,
) bool {
gvk, subresource := ctx.ResourceKind()
err := matchesResourceDescription(
err := engineutils.MatchesResourceDescription(
ctx.NewResource(),
*rule,
ctx.AdmissionInfo(),
@ -496,7 +474,7 @@ func matches(
}
oldResource := ctx.OldResource()
if oldResource.Object != nil {
err := matchesResourceDescription(
err := engineutils.MatchesResourceDescription(
ctx.OldResource(),
*rule,
ctx.AdmissionInfo(),