mirror of
https://github.com/kyverno/kyverno.git
synced 2025-01-20 18:52:16 +00:00
2cd462570a
* 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>
290 lines
10 KiB
Go
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
|
|
}
|