1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-07 00:17:13 +00:00
kyverno/pkg/background/mutate/mutate.go
Vishal Choudhary c1f8b2ed96
fix: use generate name for background scan reports (#11586)
Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>
Co-authored-by: shuting <shuting@nirmata.com>
2024-11-15 09:37:31 +00:00

302 lines
11 KiB
Go

package mutate
import (
"context"
"fmt"
"github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
kyvernov2 "github.com/kyverno/kyverno/api/kyverno/v2"
"github.com/kyverno/kyverno/pkg/background/common"
"github.com/kyverno/kyverno/pkg/breaker"
"github.com/kyverno/kyverno/pkg/client/clientset/versioned"
kyvernov1listers "github.com/kyverno/kyverno/pkg/client/listers/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/jmespath"
"github.com/kyverno/kyverno/pkg/event"
admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
engineutils "github.com/kyverno/kyverno/pkg/utils/engine"
reportutils "github.com/kyverno/kyverno/pkg/utils/report"
"go.uber.org/multierr"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
corev1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
)
var ErrEmptyPatch error = fmt.Errorf("empty resource to patch")
type mutateExistingController struct {
// clients
client dclient.Interface
kyvernoClient versioned.Interface
statusControl common.StatusControlInterface
engine engineapi.Engine
// listers
policyLister kyvernov1listers.ClusterPolicyLister
npolicyLister kyvernov1listers.PolicyLister
nsLister corev1listers.NamespaceLister
configuration config.Configuration
eventGen event.Interface
log logr.Logger
jp jmespath.Interface
reportsConfig reportutils.ReportingConfiguration
reportsBreaker breaker.Breaker
}
// NewMutateExistingController returns an instance of the MutateExistingController
func NewMutateExistingController(
client dclient.Interface,
kyvernoClient versioned.Interface,
statusControl common.StatusControlInterface,
engine engineapi.Engine,
policyLister kyvernov1listers.ClusterPolicyLister,
npolicyLister kyvernov1listers.PolicyLister,
nsLister corev1listers.NamespaceLister,
dynamicConfig config.Configuration,
eventGen event.Interface,
log logr.Logger,
jp jmespath.Interface,
reportsConfig reportutils.ReportingConfiguration,
reportsBreaker breaker.Breaker,
) *mutateExistingController {
c := mutateExistingController{
client: client,
kyvernoClient: kyvernoClient,
statusControl: statusControl,
engine: engine,
policyLister: policyLister,
npolicyLister: npolicyLister,
nsLister: nsLister,
configuration: dynamicConfig,
eventGen: eventGen,
log: log,
jp: jp,
reportsConfig: reportsConfig,
reportsBreaker: reportsBreaker,
}
return &c
}
func (c *mutateExistingController) ProcessUR(ur *kyvernov2.UpdateRequest) error {
logger := c.log.WithValues("name", ur.GetName(), "policy", ur.Spec.GetPolicyKey(), "resource", ur.Spec.GetResource().String())
var errs []error
logger.Info("processing mutate existing")
policy, err := c.getPolicy(ur)
if err != nil {
logger.Error(err, "failed to get policy")
return err
}
for _, rule := range policy.GetSpec().Rules {
if !rule.HasMutateExisting() || ur.Spec.Rule != rule.Name {
continue
}
var trigger *unstructured.Unstructured
admissionRequest := ur.Spec.Context.AdmissionRequestInfo.AdmissionRequest
if admissionRequest == nil {
trigger, err = common.GetResource(c.client, ur.Spec.Resource, ur.Spec, c.log)
if err != nil || trigger == nil {
logger.WithName(rule.Name).Error(err, "failed to get trigger resource")
if err := updateURStatus(c.statusControl, *ur, err); err != nil {
return err
}
continue
}
} else {
if admissionRequest.Operation == admissionv1.Create {
trigger, err = common.GetResource(c.client, ur.Spec.Resource, ur.Spec, c.log)
if err != nil || trigger == nil {
if admissionRequest.SubResource == "" {
logger.WithName(rule.Name).Error(err, "failed to get trigger resource")
if err := updateURStatus(c.statusControl, *ur, err); err != nil {
return err
}
continue
} else {
logger.WithName(rule.Name).Info("trigger resource not found for subresource, reverting to resource in AdmissionReviewRequest", "subresource", admissionRequest.SubResource)
newResource, _, err := admissionutils.ExtractResources(nil, *admissionRequest)
if err != nil {
logger.WithName(rule.Name).Error(err, "failed to extract resources from admission review request")
errs = append(errs, err)
continue
}
trigger = &newResource
}
}
} else {
newResource, oldResource, err := admissionutils.ExtractResources(nil, *admissionRequest)
if err != nil {
logger.WithName(rule.Name).Error(err, "failed to extract resources from admission review request")
errs = append(errs, err)
continue
}
trigger = &newResource
if newResource.Object == nil {
trigger = &oldResource
}
}
}
namespaceLabels := engineutils.GetNamespaceSelectorsFromNamespaceLister(trigger.GetKind(), trigger.GetNamespace(), c.nsLister, logger)
policyContext, err := common.NewBackgroundContext(logger, c.client, ur.Spec.Context, policy, trigger, c.configuration, c.jp, namespaceLabels)
if err != nil {
logger.WithName(rule.Name).Error(err, "failed to build policy context")
errs = append(errs, err)
continue
}
if admissionRequest != nil {
var gvk schema.GroupVersionKind
gvk, err = c.client.Discovery().GetGVKFromGVR(schema.GroupVersionResource(admissionRequest.Resource))
if err != nil {
logger.WithName(rule.Name).Error(err, "failed to get GVK from GVR", "GVR", admissionRequest.Resource)
errs = append(errs, err)
continue
}
policyContext = policyContext.WithResourceKind(gvk, admissionRequest.SubResource)
}
er := c.engine.Mutate(context.TODO(), policyContext)
if c.needsReports(trigger) {
if err := c.createReports(context.TODO(), policyContext.NewResource(), er); err != nil {
c.log.Error(err, "failed to create report")
}
}
for _, r := range er.PolicyResponse.Rules {
patched, parentGVR, patchedSubresource := r.PatchedTarget()
switch r.Status() {
case engineapi.RuleStatusFail, engineapi.RuleStatusError, engineapi.RuleStatusWarn:
err := fmt.Errorf("failed to mutate existing resource, rule %s, response %v: %s", r.Name(), r.Status(), r.Message())
logger.Error(err, "")
errs = append(errs, err)
c.report(err, policy, rule.Name, patched)
case engineapi.RuleStatusSkip:
err := fmt.Errorf("mutate existing rule skipped, rule %s, response %v: %s", r.Name(), r.Status(), r.Message())
logger.V(4).Info(err.Error())
case engineapi.RuleStatusPass:
patchedNew := patched
if patchedNew == nil {
logger.Error(ErrEmptyPatch, "", "rule", r.Name(), "message", r.Message())
errs = append(errs, ErrEmptyPatch)
continue
}
patchedNew.SetResourceVersion(patched.GetResourceVersion())
var updateErr error
if patchedSubresource == "status" {
_, updateErr = c.client.UpdateStatusResource(context.TODO(), patchedNew.GetAPIVersion(), patchedNew.GetKind(), patchedNew.GetNamespace(), patchedNew.Object, false)
} else if patchedSubresource != "" {
parentResourceGVR := parentGVR
parentResourceGV := schema.GroupVersion{Group: parentResourceGVR.Group, Version: parentResourceGVR.Version}
parentResourceGVK, err := c.client.Discovery().GetGVKFromGVR(parentResourceGV.WithResource(parentResourceGVR.Resource))
if err != nil {
logger.Error(err, "failed to get GVK from GVR", "GVR", parentResourceGVR)
errs = append(errs, err)
continue
}
_, updateErr = c.client.UpdateResource(context.TODO(), parentResourceGV.String(), parentResourceGVK.Kind, patchedNew.GetNamespace(), patchedNew.Object, false, patchedSubresource)
} else {
_, updateErr = c.client.UpdateResource(context.TODO(), patchedNew.GetAPIVersion(), patchedNew.GetKind(), patchedNew.GetNamespace(), patchedNew.Object, false)
}
if updateErr != nil {
errs = append(errs, updateErr)
logger.WithName(rule.Name).Error(updateErr, "failed to update target resource", "namespace", patchedNew.GetNamespace(), "name", patchedNew.GetName())
} else {
logger.WithName(rule.Name).V(4).Info("successfully mutated existing resource", "namespace", patchedNew.GetNamespace(), "name", patchedNew.GetName())
}
c.report(updateErr, policy, rule.Name, patched)
}
}
}
err = multierr.Combine(errs...)
return updateURStatus(c.statusControl, *ur, err)
}
func (c *mutateExistingController) getPolicy(ur *kyvernov2.UpdateRequest) (policy kyvernov1.PolicyInterface, err error) {
pNamespace, pName, err := cache.SplitMetaNamespaceKey(ur.Spec.Policy)
if err != nil {
return nil, err
}
if pNamespace != "" {
return c.npolicyLister.Policies(pNamespace).Get(pName)
}
return c.policyLister.Get(pName)
}
func (c *mutateExistingController) report(err error, policy kyvernov1.PolicyInterface, rule string, target *unstructured.Unstructured) {
var events []event.Info
if target == nil {
c.log.WithName("mutateExisting").Info("cannot generate events for empty target resource", "policy", policy.GetName(), "rule", rule)
return
}
if err != nil {
events = event.NewBackgroundFailedEvent(err, policy, rule, event.MutateExistingController,
kyvernov1.ResourceSpec{Kind: target.GetKind(), Namespace: target.GetNamespace(), Name: target.GetName()})
} else {
events = event.NewBackgroundSuccessEvent(event.MutateExistingController, policy,
[]kyvernov1.ResourceSpec{{Kind: target.GetKind(), Namespace: target.GetNamespace(), Name: target.GetName()}})
}
c.eventGen.Add(events...)
}
func (c *mutateExistingController) needsReports(trigger *unstructured.Unstructured) bool {
createReport := c.reportsConfig.MutateExistingReportsEnabled()
if trigger == nil {
return createReport
}
if !reportutils.IsGvkSupported(trigger.GroupVersionKind()) {
createReport = false
}
return createReport
}
func (c *mutateExistingController) createReports(
ctx context.Context,
resource unstructured.Unstructured,
engineResponses ...engineapi.EngineResponse,
) error {
report := reportutils.BuildMutateExistingReport(resource.GetNamespace(), resource.GroupVersionKind(), resource.GetName(), resource.GetUID(), engineResponses...)
if len(report.GetResults()) > 0 {
err := c.reportsBreaker.Do(ctx, func(ctx context.Context) error {
_, err := reportutils.CreateReport(ctx, report, c.kyvernoClient)
return err
})
if err != nil {
return err
}
}
return nil
}
func updateURStatus(statusControl common.StatusControlInterface, ur kyvernov2.UpdateRequest, err error) error {
if err != nil {
if _, err := statusControl.Failed(ur.GetName(), err.Error(), nil); err != nil {
return err
}
} else {
if _, err := statusControl.Success(ur.GetName(), nil); err != nil {
return err
}
}
return nil
}