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:
parent
873ea5c564
commit
e3b4a84c66
15 changed files with 588 additions and 396 deletions
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
185
pkg/engine/handlers/mutation/common.go
Normal file
185
pkg/engine/handlers/mutation/common.go
Normal 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())
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package engine
|
||||
package mutation
|
||||
|
||||
import (
|
||||
"context"
|
|
@ -1,4 +1,4 @@
|
|||
package engine
|
||||
package mutation
|
||||
|
||||
import (
|
||||
"fmt"
|
109
pkg/engine/handlers/mutation/mutate.go
Normal file
109
pkg/engine/handlers/mutation/mutate.go
Normal 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))
|
||||
}
|
124
pkg/engine/handlers/mutation/mutateexisting.go
Normal file
124
pkg/engine/handlers/mutation/mutateexisting.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
63
pkg/engine/utils/foreach.go
Normal file
63
pkg/engine/utils/foreach.go
Normal 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
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
Loading…
Add table
Reference in a new issue