Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add initial support for mutation trees #303

Merged
merged 2 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions pkg/engine/mutate/expression.go
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())
}
13 changes: 13 additions & 0 deletions pkg/engine/mutate/mutate.go
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...)
}
13 changes: 13 additions & 0 deletions pkg/engine/mutate/mutation.go
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)
}
156 changes: 156 additions & 0 deletions pkg/engine/mutate/parse.go
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
}
63 changes: 63 additions & 0 deletions pkg/engine/mutate/project.go
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
// }
Loading