1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-01-20 18:52:16 +00:00
kyverno/pkg/background/generate/generator.go
shuting 2cd462570a
feat: foreach support for clone (#10888)
* chore: add chainsaw tests for foreach clone

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* fix: update webhooks for foreach generate

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* chore: rename generatePattern

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* chore: chainsaw tests for generateExisting

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* chore: add missing files

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* chore: add chainsaw tests for foreach clone, sync=true

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* fix: linter issues

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* chore: add chainsaw test foreach clonelist, sync=true, delete source

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* fix: sync deletion for cloneList

Signed-off-by: ShutingZhao <shuting@nirmata.com>

---------

Signed-off-by: ShutingZhao <shuting@nirmata.com>
2024-08-29 11:59:22 +00:00

290 lines
10 KiB
Go

package generate
import (
"context"
"fmt"
"github.com/go-logr/logr"
gojmespath "github.com/kyverno/go-jmespath"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/background/common"
"github.com/kyverno/kyverno/pkg/clients/dclient"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
"github.com/kyverno/kyverno/pkg/engine/validate"
"github.com/kyverno/kyverno/pkg/engine/variables"
"go.uber.org/multierr"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
type generator struct {
client dclient.Interface
logger logr.Logger
policyContext engineapi.PolicyContext
policy kyvernov1.PolicyInterface
rule kyvernov1.Rule
contextEntries []kyvernov1.ContextEntry
anyAllConditions any
trigger unstructured.Unstructured
forEach []kyvernov1.ForEachGeneration
pattern kyvernov1.GeneratePattern
contextLoader engineapi.EngineContextLoader
}
func newGenerator(client dclient.Interface,
logger logr.Logger,
policyContext engineapi.PolicyContext,
policy kyvernov1.PolicyInterface,
rule kyvernov1.Rule,
contextEntries []kyvernov1.ContextEntry,
anyAllConditions any,
trigger unstructured.Unstructured,
pattern kyvernov1.GeneratePattern,
contextLoader engineapi.EngineContextLoader,
) *generator {
return &generator{
client: client,
logger: logger,
policyContext: policyContext,
policy: policy,
rule: rule,
contextEntries: contextEntries,
anyAllConditions: anyAllConditions,
trigger: trigger,
pattern: pattern,
contextLoader: contextLoader,
}
}
func newForeachGenerator(client dclient.Interface,
logger logr.Logger,
policyContext engineapi.PolicyContext,
policy kyvernov1.PolicyInterface,
rule kyvernov1.Rule,
contextEntries []kyvernov1.ContextEntry,
anyAllConditions any,
trigger unstructured.Unstructured,
forEach []kyvernov1.ForEachGeneration,
contextLoader engineapi.EngineContextLoader,
) *generator {
return &generator{
client: client,
logger: logger,
policyContext: policyContext,
policy: policy,
rule: rule,
contextEntries: contextEntries,
anyAllConditions: anyAllConditions,
trigger: trigger,
forEach: forEach,
contextLoader: contextLoader,
}
}
func (g *generator) generate() ([]kyvernov1.ResourceSpec, error) {
responses := []generateResponse{}
var err error
var newGenResources []kyvernov1.ResourceSpec
if err := g.loadContext(context.TODO()); err != nil {
return newGenResources, fmt.Errorf("failed to load context: %v", err)
}
typeConditions, err := engineutils.TransformConditions(g.anyAllConditions)
if err != nil {
return newGenResources, fmt.Errorf("failed to parse preconditions: %v", err)
}
preconditionsPassed, msg, err := variables.EvaluateConditions(g.logger, g.policyContext.JSONContext(), typeConditions)
if err != nil {
return newGenResources, fmt.Errorf("failed to evaluate preconditions: %v", err)
}
if !preconditionsPassed {
g.logger.V(2).Info("preconditions not met", "msg", msg)
return newGenResources, nil
}
pattern, err := variables.SubstituteAllInType(g.logger, g.policyContext.JSONContext(), &g.pattern)
if err != nil {
g.logger.Error(err, "variable substitution failed for rule", "rule", g.rule.Name)
return nil, err
}
target := pattern.ResourceSpec
logger := g.logger.WithValues("target", target.String())
if pattern.Clone.Name != "" {
resp := manageClone(logger.WithValues("type", "clone"), target, kyvernov1.ResourceSpec{}, g.policy.GetSpec().UseServerSideApply, *pattern, g.client)
responses = append(responses, resp)
} else if len(pattern.CloneList.Kinds) != 0 {
responses = manageCloneList(logger.WithValues("type", "cloneList"), target.GetNamespace(), g.policy.GetSpec().UseServerSideApply, *pattern, g.client)
} else {
resp := manageData(logger.WithValues("type", "data"), target, pattern.RawData, g.rule.Generation.Synchronize, g.client)
responses = append(responses, resp)
}
for _, response := range responses {
targetMeta := response.GetTarget()
if response.GetError() != nil {
logger.Error(response.GetError(), "failed to generate resource", "mode", response.GetAction())
return newGenResources, err
}
if response.GetAction() == Skip {
continue
}
logger.V(3).Info("applying generate rule", "mode", response.GetAction())
if response.GetData() == nil && response.GetAction() == Update {
logger.V(4).Info("no changes required for generate target resource")
return newGenResources, nil
}
newResource := &unstructured.Unstructured{}
newResource.SetUnstructuredContent(response.GetData())
newResource.SetName(targetMeta.GetName())
newResource.SetNamespace(targetMeta.GetNamespace())
if newResource.GetKind() == "" {
newResource.SetKind(targetMeta.GetKind())
}
newResource.SetAPIVersion(targetMeta.GetAPIVersion())
common.ManageLabels(newResource, g.trigger, g.policy, g.rule.Name)
if response.GetAction() == Create {
newResource.SetResourceVersion("")
if g.policy.GetSpec().UseServerSideApply {
_, err = g.client.ApplyResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), targetMeta.GetName(), newResource, false, "generate")
} else {
_, err = g.client.CreateResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), newResource, false)
}
if err != nil {
if !apierrors.IsAlreadyExists(err) {
return newGenResources, err
}
}
logger.V(2).Info("created generate target resource")
newGenResources = append(newGenResources, targetMeta)
} else if response.GetAction() == Update {
generatedObj, err := g.client.GetResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), targetMeta.GetName())
if err != nil {
logger.V(2).Info("creating new target due to the failure when fetching", "err", err.Error())
if g.policy.GetSpec().UseServerSideApply {
_, err = g.client.ApplyResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), targetMeta.GetName(), newResource, false, "generate")
} else {
_, err = g.client.CreateResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), newResource, false)
}
if err != nil {
return newGenResources, err
}
newGenResources = append(newGenResources, targetMeta)
} else {
if !g.rule.Generation.Synchronize {
logger.V(4).Info("synchronize disabled, skip syncing changes")
continue
}
if err := validate.MatchPattern(logger, newResource.Object, generatedObj.Object); err == nil {
if err := validate.MatchPattern(logger, generatedObj.Object, newResource.Object); err == nil {
logger.V(4).Info("patterns match, skipping updates")
continue
}
}
logger.V(4).Info("updating existing resource")
if targetMeta.GetAPIVersion() == "" {
generatedResourceAPIVersion := generatedObj.GetAPIVersion()
newResource.SetAPIVersion(generatedResourceAPIVersion)
}
if targetMeta.GetNamespace() == "" {
newResource.SetNamespace("default")
}
if g.policy.GetSpec().UseServerSideApply {
_, err = g.client.ApplyResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), targetMeta.GetName(), newResource, false, "generate")
} else {
_, err = g.client.UpdateResource(context.TODO(), targetMeta.GetAPIVersion(), targetMeta.GetKind(), targetMeta.GetNamespace(), newResource, false)
}
if err != nil {
logger.Error(err, "failed to update resource")
return newGenResources, err
}
}
logger.V(3).Info("updated generate target resource")
}
}
return newGenResources, nil
}
func (g *generator) generateForeach() ([]kyvernov1.ResourceSpec, error) {
var errors []error
var genResources []kyvernov1.ResourceSpec
for i, foreach := range g.forEach {
elements, err := engineutils.EvaluateList(foreach.List, g.policyContext.JSONContext())
if err != nil {
errors = append(errors, fmt.Errorf("failed to evaluate %v foreach list: %v", i, err))
continue
}
gen, err := g.generateElements(foreach, elements, nil)
if err != nil {
errors = append(errors, fmt.Errorf("failed to process %v foreach in rule %s: %v", i, g.rule.Name, err))
}
if gen != nil {
genResources = append(genResources, gen...)
}
}
return genResources, multierr.Combine(errors...)
}
func (g *generator) generateElements(foreach kyvernov1.ForEachGeneration, elements []interface{}, elementScope *bool) ([]kyvernov1.ResourceSpec, error) {
var errors []error
var genResources []kyvernov1.ResourceSpec
g.policyContext.JSONContext().Checkpoint()
defer g.policyContext.JSONContext().Restore()
for index, element := range elements {
if element == nil {
continue
}
g.policyContext.JSONContext().Reset()
policyContext := g.policyContext.Copy()
if err := engineutils.AddElementToContext(policyContext, element, index, 0, elementScope); err != nil {
g.logger.Error(err, "")
errors = append(errors, fmt.Errorf("failed to add %v element to context: %v", index, err))
continue
}
gen, err := newGenerator(g.client,
g.logger,
policyContext,
g.policy,
g.rule,
foreach.Context,
foreach.AnyAllConditions,
g.trigger,
foreach.GeneratePattern,
g.contextLoader).
generate()
if err != nil {
errors = append(errors, fmt.Errorf("failed to process %v element: %v", index, err))
}
if gen != nil {
genResources = append(genResources, gen...)
}
}
return genResources, multierr.Combine(errors...)
}
func (g *generator) loadContext(ctx context.Context) error {
if err := g.contextLoader(ctx, g.contextEntries, g.policyContext.JSONContext()); err != nil {
if _, ok := err.(gojmespath.NotFoundError); ok {
g.logger.V(3).Info("failed to load context", "reason", err.Error())
} else {
g.logger.Error(err, "failed to load context")
}
return err
}
return nil
}