diff --git a/pkg/engine/mutate/expression.go b/pkg/engine/mutate/expression.go new file mode 100644 index 00000000..4949a6f9 --- /dev/null +++ b/pkg/engine/mutate/expression.go @@ -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()) +} diff --git a/pkg/engine/mutate/mutate.go b/pkg/engine/mutate/mutate.go new file mode 100644 index 00000000..9224e4a5 --- /dev/null +++ b/pkg/engine/mutate/mutate.go @@ -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...) +} diff --git a/pkg/engine/mutate/mutation.go b/pkg/engine/mutate/mutation.go new file mode 100644 index 00000000..b712f1d1 --- /dev/null +++ b/pkg/engine/mutate/mutation.go @@ -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) +} diff --git a/pkg/engine/mutate/parse.go b/pkg/engine/mutate/parse.go new file mode 100644 index 00000000..bc732cd1 --- /dev/null +++ b/pkg/engine/mutate/parse.go @@ -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 +} diff --git a/pkg/engine/mutate/project.go b/pkg/engine/mutate/project.go new file mode 100644 index 00000000..b3f0f60d --- /dev/null +++ b/pkg/engine/mutate/project.go @@ -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 +// }