2023-03-27 10:09:46 +02:00
|
|
|
package mutation
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2023-04-13 17:34:56 +00:00
|
|
|
"encoding/json"
|
2023-03-27 10:09:46 +02:00
|
|
|
"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"
|
2023-04-13 17:34:56 +00:00
|
|
|
"github.com/kyverno/kyverno/pkg/engine/mutate/patch"
|
2023-03-27 10:09:46 +02:00
|
|
|
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
|
|
|
|
"github.com/kyverno/kyverno/pkg/utils/api"
|
2023-04-13 17:34:56 +00:00
|
|
|
"github.com/mattbaird/jsonpatch"
|
2023-03-27 10:09:46 +02:00
|
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
|
|
)
|
|
|
|
|
|
|
|
type forEachMutator struct {
|
2023-04-03 21:58:58 +02:00
|
|
|
logger logr.Logger
|
|
|
|
rule kyvernov1.Rule
|
2023-03-27 10:09:46 +02:00
|
|
|
policyContext engineapi.PolicyContext
|
|
|
|
foreach []kyvernov1.ForEachMutation
|
|
|
|
resource resourceInfo
|
|
|
|
nesting int
|
|
|
|
contextLoader engineapi.EngineContextLoader
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *forEachMutator) mutateForEach(ctx context.Context) *mutate.Response {
|
|
|
|
var applyCount int
|
|
|
|
allPatches := make([][]byte, 0)
|
|
|
|
|
|
|
|
for _, foreach := range f.foreach {
|
|
|
|
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...)
|
|
|
|
}
|
2023-04-03 21:58:58 +02:00
|
|
|
f.logger.Info("mutateResp.PatchedResource", "resource", mutateResp.PatchedResource)
|
2023-03-27 10:09:46 +02:00
|
|
|
if err := f.policyContext.JSONContext().AddResource(mutateResp.PatchedResource.Object); err != nil {
|
2023-04-03 21:58:58 +02:00
|
|
|
f.logger.Error(err, "failed to update resource in context")
|
2023-03-27 10:09:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-04-03 21:58:58 +02:00
|
|
|
preconditionsPassed, err := internal.CheckPreconditions(f.logger, policyContext.JSONContext(), foreach.AnyAllConditions)
|
2023-03-27 10:09:46 +02:00
|
|
|
if err != nil {
|
|
|
|
return mutate.NewErrorResponse(fmt.Sprintf("failed to evaluate mutate.foreach[%d].preconditions", index), err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !preconditionsPassed {
|
2023-04-03 21:58:58 +02:00
|
|
|
f.logger.Info("mutate.foreach.preconditions not met", "elementIndex", index)
|
2023-03-27 10:09:46 +02:00
|
|
|
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,
|
2023-04-03 21:58:58 +02:00
|
|
|
logger: f.logger,
|
2023-03-27 10:09:46 +02:00
|
|
|
foreach: nestedForEach,
|
|
|
|
nesting: f.nesting + 1,
|
|
|
|
contextLoader: f.contextLoader,
|
|
|
|
}
|
|
|
|
|
|
|
|
mutateResp = m.mutateForEach(ctx)
|
|
|
|
} else {
|
2023-04-03 21:58:58 +02:00
|
|
|
mutateResp = mutate.ForEach(f.rule.Name, foreach, policyContext, patchedResource.unstructured, element, f.logger)
|
2023-03-27 10:09:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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...)
|
|
|
|
}
|
|
|
|
}
|
2023-04-13 17:34:56 +00:00
|
|
|
var sortedPatches []jsonpatch.JsonPatchOperation
|
|
|
|
for _, p := range allPatches {
|
|
|
|
var jp jsonpatch.JsonPatchOperation
|
|
|
|
if err := json.Unmarshal(p, &jp); err != nil {
|
|
|
|
return mutate.NewErrorResponse("failed to convert patch", err)
|
|
|
|
}
|
|
|
|
sortedPatches = append(sortedPatches, jp)
|
|
|
|
}
|
|
|
|
sortedPatches = patch.FilterAndSortPatches(sortedPatches)
|
|
|
|
var finalPatches [][]byte
|
|
|
|
for _, p := range sortedPatches {
|
|
|
|
if data, err := p.MarshalJSON(); err != nil {
|
|
|
|
return mutate.NewErrorResponse("failed to marshal patch", err)
|
|
|
|
} else {
|
|
|
|
finalPatches = append(finalPatches, data)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return mutate.NewResponse(engineapi.RuleStatusPass, patchedResource.unstructured, finalPatches, "")
|
2023-03-27 10:09:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func buildRuleResponse(rule *kyvernov1.Rule, mutateResp *mutate.Response, info resourceInfo) *engineapi.RuleResponse {
|
2023-04-05 12:35:38 +02:00
|
|
|
message := mutateResp.Message
|
|
|
|
if mutateResp.Status == engineapi.RuleStatusPass {
|
|
|
|
message = buildSuccessMessage(mutateResp.PatchedResource)
|
2023-03-27 10:09:46 +02:00
|
|
|
}
|
2023-04-05 12:35:38 +02:00
|
|
|
resp := engineapi.NewRuleResponse(
|
|
|
|
rule.Name,
|
|
|
|
engineapi.Mutation,
|
|
|
|
message,
|
|
|
|
mutateResp.Status,
|
|
|
|
)
|
|
|
|
if mutateResp.Status == engineapi.RuleStatusPass {
|
|
|
|
resp = resp.WithPatches(mutateResp.Patches...)
|
|
|
|
if len(rule.Mutation.Targets) != 0 {
|
|
|
|
resp = resp.WithPatchedTarget(&mutateResp.PatchedResource, info.parentResourceGVR, info.subresource)
|
|
|
|
}
|
2023-03-27 10:09:46 +02:00
|
|
|
}
|
|
|
|
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())
|
|
|
|
}
|