-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add initial support for mutation trees (#303)
Signed-off-by: Charles-Edouard Brétéché <[email protected]>
- Loading branch information
1 parent
4e04d7f
commit 60f879c
Showing
5 changed files
with
310 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package mutate | ||
|
||
import ( | ||
"context" | ||
"reflect" | ||
"regexp" | ||
|
||
reflectutils "github.com/kyverno/kyverno-json/pkg/utils/reflect" | ||
) | ||
|
||
var ( | ||
foreachRegex = regexp.MustCompile(`^~(\w+)?\.(.*)`) | ||
bindingRegex = regexp.MustCompile(`(.*)\s*->\s*(\w+)$`) | ||
escapeRegex = regexp.MustCompile(`^\\(.+)\\$`) | ||
engineRegex = regexp.MustCompile(`^\((?:(\w+):)?(.+)\)$`) | ||
) | ||
|
||
type expression struct { | ||
foreach bool | ||
foreachName string | ||
statement string | ||
binding string | ||
engine string | ||
} | ||
|
||
func parseExpressionRegex(ctx context.Context, in string) *expression { | ||
expression := &expression{} | ||
// 1. match foreach | ||
if match := foreachRegex.FindStringSubmatch(in); match != nil { | ||
expression.foreach = true | ||
expression.foreachName = match[1] | ||
in = match[2] | ||
} | ||
// 2. match binding | ||
if match := bindingRegex.FindStringSubmatch(in); match != nil { | ||
expression.binding = match[2] | ||
in = match[1] | ||
} | ||
// 3. match escape, if there's no escaping then match engine | ||
if match := escapeRegex.FindStringSubmatch(in); match != nil { | ||
in = match[1] | ||
} else { | ||
if match := engineRegex.FindStringSubmatch(in); match != nil { | ||
expression.engine = match[1] | ||
// account for default engine | ||
if expression.engine == "" { | ||
expression.engine = "jp" | ||
} | ||
in = match[2] | ||
} | ||
} | ||
// parse statement | ||
expression.statement = in | ||
if expression.statement == "" { | ||
return nil | ||
} | ||
return expression | ||
} | ||
|
||
func parseExpression(ctx context.Context, value any) *expression { | ||
if reflectutils.GetKind(value) != reflect.String { | ||
return nil | ||
} | ||
return parseExpressionRegex(ctx, reflect.ValueOf(value).String()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package mutate | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/jmespath-community/go-jmespath/pkg/binding" | ||
"github.com/kyverno/kyverno-json/pkg/engine/template" | ||
"k8s.io/apimachinery/pkg/util/validation/field" | ||
) | ||
|
||
func Mutate(ctx context.Context, path *field.Path, mutation Mutation, value any, bindings binding.Bindings, opts ...template.Option) (any, error) { | ||
return mutation.mutate(ctx, path, value, bindings, opts...) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package mutate | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/jmespath-community/go-jmespath/pkg/binding" | ||
"github.com/kyverno/kyverno-json/pkg/engine/template" | ||
"k8s.io/apimachinery/pkg/util/validation/field" | ||
) | ||
|
||
type Mutation interface { | ||
mutate(context.Context, *field.Path, any, binding.Bindings, ...template.Option) (any, error) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
package mutate | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"reflect" | ||
|
||
"github.com/jmespath-community/go-jmespath/pkg/binding" | ||
"github.com/kyverno/kyverno-json/pkg/engine/template" | ||
reflectutils "github.com/kyverno/kyverno-json/pkg/utils/reflect" | ||
"k8s.io/apimachinery/pkg/util/validation/field" | ||
) | ||
|
||
func Parse(ctx context.Context, mutation any) Mutation { | ||
switch reflectutils.GetKind(mutation) { | ||
case reflect.Slice: | ||
node := sliceNode{} | ||
valueOf := reflect.ValueOf(mutation) | ||
for i := 0; i < valueOf.Len(); i++ { | ||
node = append(node, Parse(ctx, valueOf.Index(i).Interface())) | ||
} | ||
return node | ||
case reflect.Map: | ||
node := mapNode{} | ||
iter := reflect.ValueOf(mutation).MapRange() | ||
for iter.Next() { | ||
node[iter.Key().Interface()] = Parse(ctx, iter.Value().Interface()) | ||
} | ||
return node | ||
default: | ||
return &scalarNode{rhs: mutation} | ||
} | ||
} | ||
|
||
// mapNode is the mutation type represented by a map. | ||
// it is responsible for projecting the analysed resource and passing the result to the descendant | ||
type mapNode map[any]Mutation | ||
|
||
func (n mapNode) mutate(ctx context.Context, path *field.Path, value any, bindings binding.Bindings, opts ...template.Option) (any, error) { | ||
out := map[any]any{} | ||
for k, v := range n { | ||
// TODO: very simple implementation | ||
mapValue := reflect.ValueOf(value).MapIndex(reflect.ValueOf(k)) | ||
if !mapValue.IsValid() { | ||
continue | ||
} | ||
value := mapValue.Interface() | ||
// TODO: does it make sense to take valueOf.Index(i).Interface() here ? | ||
if inner, err := v.mutate(ctx, path.Child(fmt.Sprint(k)), value, bindings, opts...); err != nil { | ||
return nil, err | ||
} else { | ||
out[k] = inner | ||
} | ||
// projection, err := project(ctx, k, value, bindings, opts...) | ||
// if err != nil { | ||
// return nil, field.InternalError(path.Child(fmt.Sprint(k)), err) | ||
// } else { | ||
// if projection.binding != "" { | ||
// bindings = bindings.Register("$"+projection.binding, jpbinding.NewBinding(projection.result)) | ||
// } | ||
// if projection.foreach { | ||
// projectedKind := reflectutils.GetKind(projection.result) | ||
// if projectedKind == reflect.Slice { | ||
// valueOf := reflect.ValueOf(projection.result) | ||
// for i := 0; i < valueOf.Len(); i++ { | ||
// bindings := bindings | ||
// if projection.foreachName != "" { | ||
// bindings = bindings.Register("$"+projection.foreachName, jpbinding.NewBinding(i)) | ||
// } | ||
// if _errs, err := v.mutate(ctx, path.Child(fmt.Sprint(k)).Index(i), valueOf.Index(i).Interface(), bindings, opts...); err != nil { | ||
// return nil, err | ||
// } else { | ||
// errs = append(errs, _errs...) | ||
// } | ||
// } | ||
// } else if projectedKind == reflect.Map { | ||
// iter := reflect.ValueOf(projection.result).MapRange() | ||
// for iter.Next() { | ||
// key := iter.Key().Interface() | ||
// bindings := bindings | ||
// if projection.foreachName != "" { | ||
// bindings = bindings.Register("$"+projection.foreachName, jpbinding.NewBinding(key)) | ||
// } | ||
// if _errs, err := v.mutate(ctx, path.Child(fmt.Sprint(k)).Key(fmt.Sprint(key)), iter.Value().Interface(), bindings, opts...); err != nil { | ||
// return nil, err | ||
// } else { | ||
// errs = append(errs, _errs...) | ||
// } | ||
// } | ||
// } else { | ||
// return nil, field.TypeInvalid(path.Child(fmt.Sprint(k)), projection.result, "expected a slice or a map") | ||
// } | ||
// } else { | ||
// if _errs, err := v.mutate(ctx, path.Child(fmt.Sprint(k)), projection.result, bindings, opts...); err != nil { | ||
// return nil, err | ||
// } else { | ||
// errs = append(errs, _errs...) | ||
// } | ||
// } | ||
// } | ||
} | ||
return out, nil | ||
} | ||
|
||
// sliceNode is the mutation type represented by a slice. | ||
// it first compares the length of the analysed resource with the length of the descendants. | ||
// if lengths match all descendants are evaluated with their corresponding items. | ||
type sliceNode []Mutation | ||
|
||
func (n sliceNode) mutate(ctx context.Context, path *field.Path, value any, bindings binding.Bindings, opts ...template.Option) (any, error) { | ||
if value == nil { | ||
return nil, nil | ||
} else if reflectutils.GetKind(value) != reflect.Slice { | ||
return nil, field.TypeInvalid(path, value, "expected a slice") | ||
} else { | ||
var out []any | ||
valueOf := reflect.ValueOf(value) | ||
for i := range n { | ||
// TODO: does it make sense to take valueOf.Index(i).Interface() here ? | ||
if inner, err := n[i].mutate(ctx, path.Index(i), valueOf.Index(i).Interface(), bindings, opts...); err != nil { | ||
return nil, err | ||
} else { | ||
out = append(out, inner) | ||
} | ||
} | ||
return out, nil | ||
} | ||
} | ||
|
||
// scalarNode is a terminal type of mutation. | ||
// it receives a value and compares it with an expected value. | ||
// the expected value can be the result of an expression. | ||
type scalarNode struct { | ||
rhs any | ||
} | ||
|
||
func (n *scalarNode) mutate(ctx context.Context, path *field.Path, value any, bindings binding.Bindings, opts ...template.Option) (any, error) { | ||
rhs := n.rhs | ||
expression := parseExpression(ctx, rhs) | ||
// we only project if the expression uses the engine syntax | ||
// this is to avoid the case where the value is a map and the RHS is a string | ||
if expression != nil && expression.engine != "" { | ||
if expression.foreachName != "" { | ||
return nil, field.Invalid(path, rhs, "foreach is not supported on the RHS") | ||
} | ||
if expression.binding != "" { | ||
return nil, field.Invalid(path, rhs, "binding is not supported on the RHS") | ||
} | ||
projected, err := template.Execute(ctx, expression.statement, value, bindings, opts...) | ||
if err != nil { | ||
return nil, field.InternalError(path, err) | ||
} | ||
rhs = projected | ||
} | ||
return rhs, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package mutate | ||
|
||
// import ( | ||
// "context" | ||
// "reflect" | ||
|
||
// "github.com/jmespath-community/go-jmespath/pkg/binding" | ||
// "github.com/kyverno/kyverno-json/pkg/engine/template" | ||
// reflectutils "github.com/kyverno/kyverno-json/pkg/utils/reflect" | ||
// ) | ||
|
||
// type projection struct { | ||
// foreach bool | ||
// foreachName string | ||
// binding string | ||
// result any | ||
// } | ||
|
||
// func project(ctx context.Context, key any, value any, bindings binding.Bindings, opts ...template.Option) (*projection, error) { | ||
// expression := parseExpression(ctx, key) | ||
// if expression != nil { | ||
// if expression.engine != "" { | ||
// projected, err := template.Execute(ctx, expression.statement, value, bindings, opts...) | ||
// if err != nil { | ||
// return nil, err | ||
// } | ||
// return &projection{ | ||
// foreach: expression.foreach, | ||
// foreachName: expression.foreachName, | ||
// binding: expression.binding, | ||
// result: projected, | ||
// }, nil | ||
// } else { | ||
// if reflectutils.GetKind(value) == reflect.Map { | ||
// mapValue := reflect.ValueOf(value).MapIndex(reflect.ValueOf(expression.statement)) | ||
// var value any | ||
// if mapValue.IsValid() { | ||
// value = mapValue.Interface() | ||
// } | ||
// return &projection{ | ||
// foreach: expression.foreach, | ||
// foreachName: expression.foreachName, | ||
// binding: expression.binding, | ||
// result: value, | ||
// }, nil | ||
// } | ||
// } | ||
// } | ||
// if reflectutils.GetKind(value) == reflect.Map { | ||
// mapValue := reflect.ValueOf(value).MapIndex(reflect.ValueOf(key)) | ||
// var value any | ||
// if mapValue.IsValid() { | ||
// value = mapValue.Interface() | ||
// } | ||
// return &projection{ | ||
// result: value, | ||
// }, nil | ||
// } | ||
// // TODO is this an error ? | ||
// return &projection{ | ||
// result: value, | ||
// }, nil | ||
// } |