From 7b1ae4d336d5b6fa9dfdc9bfc81c5572ad5cacc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Edouard=20Br=C3=A9t=C3=A9ch=C3=A9?= Date: Mon, 2 Oct 2023 17:39:24 +0200 Subject: [PATCH] lazy binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Charles-Edouard Brétéché --- config/crds/json.kyverno.io_policies.yaml | 10 ------ go.mod | 2 +- go.sum | 4 +-- pkg/apis/v1alpha1/policy.go | 9 ----- pkg/data/crds/json.kyverno.io_policies.yaml | 10 ------ pkg/engine/assert/map.go | 10 +++--- pkg/engine/template/binding.go | 38 +++++++++++++++++++++ pkg/json-engine/engine.go | 12 +++---- testdata/pod-no-latest/policy.yaml | 6 +++- 9 files changed, 57 insertions(+), 44 deletions(-) create mode 100644 pkg/engine/template/binding.go diff --git a/config/crds/json.kyverno.io_policies.yaml b/config/crds/json.kyverno.io_policies.yaml index b0edf5be..54650dde 100644 --- a/config/crds/json.kyverno.io_policies.yaml +++ b/config/crds/json.kyverno.io_policies.yaml @@ -52,16 +52,6 @@ spec: description: Variable defines an arbitrary JMESPath context variable that can be defined inline. properties: - default: - description: Default is an optional arbitrary JSON - object that the variable may take if the JMESPath - expression evaluates to nil - type: object - x-kubernetes-preserve-unknown-fields: true - jmesPath: - description: JMESPath is an optional JMESPath Expression - that can be used to transform the variable. - type: string value: description: Value is any arbitrary JSON object representable in YAML or JSON form. diff --git a/go.mod b/go.mod index c729b5f5..f09cbd94 100644 --- a/go.mod +++ b/go.mod @@ -302,4 +302,4 @@ require ( sigs.k8s.io/yaml v1.3.0 // indirect ) -replace github.com/jmespath-community/go-jmespath => github.com/eddycharly/go-community-jmespath v1.1.1-0.20230929121033-cb50a1973e28 +replace github.com/jmespath-community/go-jmespath => github.com/eddycharly/go-community-jmespath v1.1.1-0.20231002150022-0274ad918d4c diff --git a/go.sum b/go.sum index 8664c89c..9bbb1bdd 100644 --- a/go.sum +++ b/go.sum @@ -413,8 +413,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/ebitengine/purego v0.6.0-alpha h1:7skbuQ4040gPjIt6Dmvreeyjuuy+xIK1CW5cssLg0+o= github.com/ebitengine/purego v0.6.0-alpha/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= -github.com/eddycharly/go-community-jmespath v1.1.1-0.20230929121033-cb50a1973e28 h1:i3+LhieWZ+/ZUKlO7VNWuAsdzu2dIepsqT8Qg7z0X/w= -github.com/eddycharly/go-community-jmespath v1.1.1-0.20230929121033-cb50a1973e28/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I= +github.com/eddycharly/go-community-jmespath v1.1.1-0.20231002150022-0274ad918d4c h1:nWV1tE2FXHvhtoJLBoq9E8hswfGr1ufj64PH2jal8+I= +github.com/eddycharly/go-community-jmespath v1.1.1-0.20231002150022-0274ad918d4c/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= diff --git a/pkg/apis/v1alpha1/policy.go b/pkg/apis/v1alpha1/policy.go index 32f35328..efbe2e2f 100644 --- a/pkg/apis/v1alpha1/policy.go +++ b/pkg/apis/v1alpha1/policy.go @@ -63,15 +63,6 @@ type Variable struct { // +kubebuilder:pruning:PreserveUnknownFields // +kubebuilder:validation:Schemaless Value interface{} `json:"value,omitempty"` - - // JMESPath is an optional JMESPath Expression that can be used to transform the variable. - JMESPath string `json:"jmesPath,omitempty"` - - // Default is an optional arbitrary JSON object that the variable may take if the JMESPath expression evaluates to nil - // +kubebuilder:validation:Type=object - // +kubebuilder:pruning:PreserveUnknownFields - // +kubebuilder:validation:Schemaless - Default interface{} `json:"default,omitempty"` } type MatchResources struct { diff --git a/pkg/data/crds/json.kyverno.io_policies.yaml b/pkg/data/crds/json.kyverno.io_policies.yaml index b0edf5be..54650dde 100644 --- a/pkg/data/crds/json.kyverno.io_policies.yaml +++ b/pkg/data/crds/json.kyverno.io_policies.yaml @@ -52,16 +52,6 @@ spec: description: Variable defines an arbitrary JMESPath context variable that can be defined inline. properties: - default: - description: Default is an optional arbitrary JSON - object that the variable may take if the JMESPath - expression evaluates to nil - type: object - x-kubernetes-preserve-unknown-fields: true - jmesPath: - description: JMESPath is an optional JMESPath Expression - that can be used to transform the variable. - type: string value: description: Value is any arbitrary JSON object representable in YAML or JSON form. diff --git a/pkg/engine/assert/map.go b/pkg/engine/assert/map.go index 4f6f6f83..4f728782 100644 --- a/pkg/engine/assert/map.go +++ b/pkg/engine/assert/map.go @@ -5,7 +5,7 @@ import ( "reflect" reflectutils "github.com/eddycharly/json-kyverno/pkg/utils/reflect" - "github.com/jmespath-community/go-jmespath/pkg/binding" + jpbinding "github.com/jmespath-community/go-jmespath/pkg/binding" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -13,7 +13,7 @@ import ( // it is reponsible for projecting the analysed resource and passing the result to the descendant type mapNode map[interface{}]Assertion -func (n mapNode) assert(path *field.Path, value interface{}, bindings binding.Bindings) (field.ErrorList, error) { +func (n mapNode) assert(path *field.Path, value interface{}, bindings jpbinding.Bindings) (field.ErrorList, error) { var errs field.ErrorList for k, v := range n { projected, foreach, binding, err := project(k, value, bindings) @@ -21,14 +21,14 @@ func (n mapNode) assert(path *field.Path, value interface{}, bindings binding.Bi return nil, field.InternalError(path.Child(fmt.Sprint(k)), err) } else { if binding != "" { - bindings = bindings.Register("$"+binding, projected) + bindings = bindings.Register("$"+binding, jpbinding.NewBinding(projected)) } if foreach != "" { projectedKind := reflectutils.GetKind(projected) if projectedKind == reflect.Slice { valueOf := reflect.ValueOf(projected) for i := 0; i < valueOf.Len(); i++ { - bindings := bindings.Register("$"+foreach, i) + bindings := bindings.Register("$"+foreach, jpbinding.NewBinding(i)) if _errs, err := v.assert(path.Child(fmt.Sprint(k)).Index(i), valueOf.Index(i).Interface(), bindings); err != nil { return nil, err } else { @@ -39,7 +39,7 @@ func (n mapNode) assert(path *field.Path, value interface{}, bindings binding.Bi iter := reflect.ValueOf(projected).MapRange() for iter.Next() { key := iter.Key().Interface() - bindings := bindings.Register("$"+foreach, key) + bindings := bindings.Register("$"+foreach, jpbinding.NewBinding(key)) if _errs, err := v.assert(path.Child(fmt.Sprint(k)).Key(fmt.Sprint(key)), iter.Value().Interface(), bindings); err != nil { return nil, err } else { diff --git a/pkg/engine/template/binding.go b/pkg/engine/template/binding.go new file mode 100644 index 00000000..dfd94ee2 --- /dev/null +++ b/pkg/engine/template/binding.go @@ -0,0 +1,38 @@ +package template + +import ( + "sync" + + "github.com/jmespath-community/go-jmespath/pkg/binding" +) + +type resolverFunc = func() (interface{}, error) + +type lazyBinding struct { + resolver resolverFunc +} + +func (b *lazyBinding) Value() (interface{}, error) { + return b.resolver() +} + +func NewLazyBinding(resolver resolverFunc) binding.Binding { + binding := &lazyBinding{} + lock := &sync.Mutex{} + binding.resolver = func() (interface{}, error) { + lock.Lock() + defer lock.Unlock() + value, err := resolver() + binding.resolver = func() (interface{}, error) { + return value, err + } + return binding.resolver() + } + return binding +} + +func NewLazyBindingWithValue(value interface{}) binding.Binding { + return NewLazyBinding(func() (interface{}, error) { + return value, nil + }) +} diff --git a/pkg/json-engine/engine.go b/pkg/json-engine/engine.go index c1ee5d07..9a92ad71 100644 --- a/pkg/json-engine/engine.go +++ b/pkg/json-engine/engine.go @@ -10,7 +10,7 @@ import ( "github.com/eddycharly/json-kyverno/pkg/engine/builder" "github.com/eddycharly/json-kyverno/pkg/engine/match" "github.com/eddycharly/json-kyverno/pkg/engine/template" - "github.com/jmespath-community/go-jmespath/pkg/binding" + jpbinding "github.com/jmespath-community/go-jmespath/pkg/binding" ) type JsonEngineRequest struct { @@ -54,12 +54,12 @@ func New() engine.Engine[JsonEngineRequest, JsonEngineResponse] { Rule: r.Rule, Resource: r.Resource, } - bindings := binding.NewBindings() - bindings = bindings.Register("$resource", r.Resource) - bindings = bindings.Register("$rule", r.Rule) - bindings = bindings.Register("$policy", r.Policy) + bindings := jpbinding.NewBindings() + bindings = bindings.Register("$resource", jpbinding.NewBinding(r.Resource)) + bindings = bindings.Register("$rule", jpbinding.NewBinding(r.Rule)) + bindings = bindings.Register("$policy", jpbinding.NewBinding(r.Policy)) for _, entry := range r.Rule.Context { - bindings = bindings.Register("$"+entry.Name, entry.Variable.Value) + bindings = bindings.Register("$"+entry.Name, template.NewLazyBindingWithValue(entry.Variable.Value)) } errs, err := assert.Assert(r.Rule.Validation.Pattern, r.Resource, bindings) if err != nil { diff --git a/testdata/pod-no-latest/policy.yaml b/testdata/pod-no-latest/policy.yaml index d9ef1dac..01fdcc70 100644 --- a/testdata/pod-no-latest/policy.yaml +++ b/testdata/pod-no-latest/policy.yaml @@ -5,6 +5,10 @@ metadata: spec: rules: - name: pod-no-latest + context: + - name: tag + variable: + value: :latest match: any: - resource: @@ -18,7 +22,7 @@ spec: # an image tag is required (contains($foo, ':')): true # using a mutable image tag e.g. 'latest' is not allowed - (ends_with($foo, ':latest')): false + (ends_with($foo, $tag)): false ~.containers@foo: image: # an image tag is required