diff --git a/.crds/json.kyverno.io_validatingpolicies.yaml b/.crds/json.kyverno.io_validatingpolicies.yaml index afc418639..476238d5d 100644 --- a/.crds/json.kyverno.io_validatingpolicies.yaml +++ b/.crds/json.kyverno.io_validatingpolicies.yaml @@ -61,6 +61,9 @@ spec: message: description: Message is the message associated message. type: string + with: + description: With defines the data to work with. + type: string required: - check type: object @@ -79,6 +82,9 @@ spec: message: description: Message is the message associated message. type: string + with: + description: With defines the data to work with. + type: string required: - check type: object diff --git a/pkg/apis/policy/v1alpha1/assertion.go b/pkg/apis/policy/v1alpha1/assertion.go index 474e26bd2..ec782bb15 100644 --- a/pkg/apis/policy/v1alpha1/assertion.go +++ b/pkg/apis/policy/v1alpha1/assertion.go @@ -2,6 +2,10 @@ package v1alpha1 // Assertion contains an assertion tree associated with a message. type Assertion struct { + // With defines the data to work with. + // +optional + With string `json:"with,omitempty"` + // Message is the message associated message. // +optional Message string `json:"message,omitempty"` diff --git a/pkg/data/crds/json.kyverno.io_validatingpolicies.yaml b/pkg/data/crds/json.kyverno.io_validatingpolicies.yaml index afc418639..476238d5d 100644 --- a/pkg/data/crds/json.kyverno.io_validatingpolicies.yaml +++ b/pkg/data/crds/json.kyverno.io_validatingpolicies.yaml @@ -61,6 +61,9 @@ spec: message: description: Message is the message associated message. type: string + with: + description: With defines the data to work with. + type: string required: - check type: object @@ -79,6 +82,9 @@ spec: message: description: Message is the message associated message. type: string + with: + description: With defines the data to work with. + type: string required: - check type: object diff --git a/pkg/engine/assert/binding.go b/pkg/engine/assert/binding.go index fef9570c3..f312e1c83 100644 --- a/pkg/engine/assert/binding.go +++ b/pkg/engine/assert/binding.go @@ -11,15 +11,15 @@ import ( func NewContextBinding(path *field.Path, bindings binding.Bindings, value any, entry any) binding.Binding { return template.NewLazyBinding( func() (any, error) { - expression := parseExpression(context.TODO(), entry) + expression := ParseExpression(context.TODO(), entry) if expression != nil && expression.engine != "" { - if expression.foreach { + if expression.Foreach { return nil, field.Invalid(path.Child("variable"), entry, "foreach is not supported in context") } if expression.binding != "" { return nil, field.Invalid(path.Child("variable"), entry, "binding is not supported in context") } - projected, err := template.Execute(context.Background(), expression.statement, value, bindings) + projected, err := template.Execute(context.Background(), expression.Statement, value, bindings) if err != nil { return nil, field.InternalError(path.Child("variable"), err) } diff --git a/pkg/engine/assert/expression.go b/pkg/engine/assert/expression.go index 898fb5b5d..fc1cde96f 100644 --- a/pkg/engine/assert/expression.go +++ b/pkg/engine/assert/expression.go @@ -15,20 +15,20 @@ var ( engineRegex = regexp.MustCompile(`^\((?:(\w+):)?(.+)\)$`) ) -type expression struct { - foreach bool - foreachName string - statement string +type Expression struct { + Foreach bool + ForeachName string + Statement string binding string engine string } -func parseExpressionRegex(_ context.Context, in string) *expression { - expression := &expression{} +func parseExpressionRegex(_ context.Context, in string) *Expression { + expression := &Expression{} // 1. match foreach if match := foreachRegex.FindStringSubmatch(in); match != nil { - expression.foreach = true - expression.foreachName = match[1] + expression.Foreach = true + expression.ForeachName = match[1] in = match[2] } // 2. match binding @@ -50,14 +50,14 @@ func parseExpressionRegex(_ context.Context, in string) *expression { } } // parse statement - expression.statement = in - if expression.statement == "" { + expression.Statement = in + if expression.Statement == "" { return nil } return expression } -func parseExpression(ctx context.Context, value any) *expression { +func ParseExpression(ctx context.Context, value any) *Expression { if reflectutils.GetKind(value) != reflect.String { return nil } diff --git a/pkg/engine/assert/expression_test.go b/pkg/engine/assert/expression_test.go index 20e8c1a33..2bc6f38b8 100644 --- a/pkg/engine/assert/expression_test.go +++ b/pkg/engine/assert/expression_test.go @@ -11,7 +11,7 @@ func Test_parseExpressionRegex(t *testing.T) { tests := []struct { name string in string - want *expression + want *Expression }{{ name: "empty", in: "", @@ -19,153 +19,153 @@ func Test_parseExpressionRegex(t *testing.T) { }, { name: "simple field", in: "test", - want: &expression{ - foreach: false, - foreachName: "", - statement: "test", + want: &Expression{ + Foreach: false, + ForeachName: "", + Statement: "test", }, }, { name: "simple field", in: "(test)", - want: &expression{ - foreach: false, - foreachName: "", - statement: "test", + want: &Expression{ + Foreach: false, + ForeachName: "", + Statement: "test", engine: "jp", }, }, { name: "nested field", in: "test.test", - want: &expression{ - foreach: false, - foreachName: "", - statement: "test.test", + want: &Expression{ + Foreach: false, + ForeachName: "", + Statement: "test.test", }, }, { name: "nested field", in: "(test.test)", - want: &expression{ - foreach: false, - foreachName: "", - statement: "test.test", + want: &Expression{ + Foreach: false, + ForeachName: "", + Statement: "test.test", engine: "jp", }, }, { name: "foreach simple field", in: "~.test", - want: &expression{ - foreach: true, - foreachName: "", - statement: "test", + want: &Expression{ + Foreach: true, + ForeachName: "", + Statement: "test", }, }, { name: "foreach simple field", in: "~.(test)", - want: &expression{ - foreach: true, - foreachName: "", - statement: "test", + want: &Expression{ + Foreach: true, + ForeachName: "", + Statement: "test", engine: "jp", }, }, { name: "foreach nested field", in: "~.(test.test)", - want: &expression{ - foreach: true, - foreachName: "", - statement: "test.test", + want: &Expression{ + Foreach: true, + ForeachName: "", + Statement: "test.test", engine: "jp", }, }, { name: "binding", in: "test->foo", - want: &expression{ - foreach: false, - foreachName: "", - statement: "test", + want: &Expression{ + Foreach: false, + ForeachName: "", + Statement: "test", binding: "foo", }, }, { name: "binding", in: "(test)->foo", - want: &expression{ - foreach: false, - foreachName: "", - statement: "test", + want: &Expression{ + Foreach: false, + ForeachName: "", + Statement: "test", binding: "foo", engine: "jp", }, }, { name: "foreach and binding", in: "~.test->foo", - want: &expression{ - foreach: true, - foreachName: "", - statement: "test", + want: &Expression{ + Foreach: true, + ForeachName: "", + Statement: "test", binding: "foo", }, }, { name: "foreach and binding", in: "~.(test)->foo", - want: &expression{ - foreach: true, - foreachName: "", - statement: "test", + want: &Expression{ + Foreach: true, + ForeachName: "", + Statement: "test", binding: "foo", engine: "jp", }, }, { name: "escape", in: `\~(test)->foo\`, - want: &expression{ - foreach: false, - foreachName: "", - statement: "~(test)->foo", + want: &Expression{ + Foreach: false, + ForeachName: "", + Statement: "~(test)->foo", binding: "", }, }, { name: "escape", in: `\test\`, - want: &expression{ - foreach: false, - foreachName: "", - statement: "test", + want: &Expression{ + Foreach: false, + ForeachName: "", + Statement: "test", binding: "", }, }, { name: "escape", in: `\(test)\`, - want: &expression{ - foreach: false, - foreachName: "", - statement: "(test)", + want: &Expression{ + Foreach: false, + ForeachName: "", + Statement: "(test)", binding: "", }, }, { name: "escape", in: `\/test/\`, - want: &expression{ - foreach: false, - foreachName: "", - statement: "/test/", + want: &Expression{ + Foreach: false, + ForeachName: "", + Statement: "/test/", binding: "", }, }, { name: "escape", in: `~index.\(test)\`, - want: &expression{ - foreach: true, - foreachName: "index", - statement: "(test)", + want: &Expression{ + Foreach: true, + ForeachName: "index", + Statement: "(test)", binding: "", }, }, { name: "escape", in: `~index.\(test)\->name`, - want: &expression{ - foreach: true, - foreachName: "index", - statement: "(test)", + want: &Expression{ + Foreach: true, + ForeachName: "index", + Statement: "(test)", binding: "name", }, }} diff --git a/pkg/engine/assert/parse.go b/pkg/engine/assert/parse.go index 8d71286e4..b9c461bfd 100644 --- a/pkg/engine/assert/parse.go +++ b/pkg/engine/assert/parse.go @@ -138,17 +138,17 @@ type scalarNode struct { func (n *scalarNode) assert(ctx context.Context, path *field.Path, value any, bindings binding.Bindings, opts ...template.Option) (field.ErrorList, error) { rhs := n.rhs - expression := parseExpression(ctx, 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 != "" { + 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...) + projected, err := template.Execute(ctx, expression.Statement, value, bindings, opts...) if err != nil { return nil, field.InternalError(path, err) } diff --git a/pkg/engine/assert/project.go b/pkg/engine/assert/project.go index 1a36f81dd..89f4510ba 100644 --- a/pkg/engine/assert/project.go +++ b/pkg/engine/assert/project.go @@ -18,16 +18,16 @@ type projection struct { } func project(ctx context.Context, key any, value any, bindings binding.Bindings, opts ...template.Option) (*projection, error) { - expression := parseExpression(ctx, key) + expression := ParseExpression(ctx, key) if expression != nil { if expression.engine != "" { - projected, err := template.Execute(ctx, expression.statement, value, bindings, opts...) + projected, err := template.Execute(ctx, expression.Statement, value, bindings, opts...) if err != nil { return nil, err } return &projection{ - foreach: expression.foreach, - foreachName: expression.foreachName, + foreach: expression.Foreach, + foreachName: expression.ForeachName, binding: expression.binding, result: projected, }, nil @@ -35,13 +35,13 @@ func project(ctx context.Context, key any, value any, bindings binding.Bindings, if value == nil { return nil, nil } else if reflectutils.GetKind(value) == reflect.Map { - mapValue := reflect.ValueOf(value).MapIndex(reflect.ValueOf(expression.statement)) + mapValue := reflect.ValueOf(value).MapIndex(reflect.ValueOf(expression.Statement)) if !mapValue.IsValid() { return nil, nil } return &projection{ - foreach: expression.foreach, - foreachName: expression.foreachName, + foreach: expression.Foreach, + foreachName: expression.ForeachName, binding: expression.binding, result: mapValue.Interface(), }, nil diff --git a/pkg/matching/match.go b/pkg/matching/match.go index 3723e0a05..1daaed5e4 100644 --- a/pkg/matching/match.go +++ b/pkg/matching/match.go @@ -2,12 +2,14 @@ package matching import ( "context" + "reflect" "strings" "github.com/jmespath-community/go-jmespath/pkg/binding" "github.com/kyverno/kyverno-json/pkg/apis/policy/v1alpha1" "github.com/kyverno/kyverno-json/pkg/engine/assert" "github.com/kyverno/kyverno-json/pkg/engine/template" + reflectutils "github.com/kyverno/kyverno-json/pkg/utils/reflect" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -39,7 +41,60 @@ func (r Results) Error() string { return strings.Join(lines, "\n") } -// func MatchAssert(ctx context.Context, path *field.Path, match *v1alpha1.Assert, actual any, bindings binding.Bindings, opts ...template.Option) ([]error, error) { +func MatchAssertion(ctx context.Context, path *field.Path, assertion v1alpha1.Assertion, actual any, bindings binding.Bindings, opts ...template.Option) ([]Result, error) { + foreach := false + if assertion.With != "" { + expression := assert.ParseExpression(ctx, assertion.With) + if expression != nil { + with, err := template.Execute(ctx, expression.Statement, actual, bindings, opts...) + if err != nil { + return nil, err + } + actual = with + foreach = expression.Foreach + } + } + var errs []Result + if foreach { + if actual == nil { + // errs = append(errs, field.Invalid(path, actual, "value is null")) + } else if reflectutils.GetKind(actual) != reflect.Slice { + return errs, field.TypeInvalid(path, actual, "expected a slice") + } else { + valueOf := reflect.ValueOf(actual) + for i := range valueOf.Len() { + actual := valueOf.Index(i).Interface() + if _errs, err := assert.Assert(ctx, path, assert.Parse(ctx, assertion.Check.Value), actual, bindings, opts...); err != nil { + return errs, err + } else if len(_errs) > 0 { + fail := Result{ + ErrorList: _errs, + } + if assertion.Message != "" { + fail.Message = template.String(ctx, assertion.Message, actual, bindings, opts...) + } + errs = append(errs, fail) + } + } + } + } else { + _errs, err := assert.Assert(ctx, path, assert.Parse(ctx, assertion.Check.Value), actual, bindings, opts...) + if err != nil { + return errs, err + } + if len(_errs) > 0 { + fail := Result{ + ErrorList: _errs, + } + if assertion.Message != "" { + fail.Message = template.String(ctx, assertion.Message, actual, bindings, opts...) + } + errs = append(errs, fail) + } + } + return errs, nil +} + func MatchAssert(ctx context.Context, path *field.Path, match *v1alpha1.Assert, actual any, bindings binding.Bindings, opts ...template.Option) ([]Result, error) { if match == nil || (len(match.Any) == 0 && len(match.All) == 0) { return nil, field.Invalid(path, match, "an empty assert is not valid") @@ -48,7 +103,7 @@ func MatchAssert(ctx context.Context, path *field.Path, match *v1alpha1.Assert, var fails []Result path := path.Child("any") for i, assertion := range match.Any { - checkFails, err := assert.Assert(ctx, path.Index(i).Child("check"), assert.Parse(ctx, assertion.Check.Value), actual, bindings, opts...) + checkFails, err := MatchAssertion(ctx, path.Index(i).Child("check"), assertion, actual, bindings, opts...) if err != nil { return fails, err } @@ -56,13 +111,7 @@ func MatchAssert(ctx context.Context, path *field.Path, match *v1alpha1.Assert, fails = nil break } - fail := Result{ - ErrorList: checkFails, - } - if assertion.Message != "" { - fail.Message = template.String(ctx, assertion.Message, actual, bindings, opts...) - } - fails = append(fails, fail) + fails = append(fails, checkFails...) } if fails != nil { return fails, nil @@ -72,18 +121,12 @@ func MatchAssert(ctx context.Context, path *field.Path, match *v1alpha1.Assert, var fails []Result path := path.Child("all") for i, assertion := range match.All { - checkFails, err := assert.Assert(ctx, path.Index(i).Child("check"), assert.Parse(ctx, assertion.Check.Value), actual, bindings, opts...) + checkFails, err := MatchAssertion(ctx, path.Index(i).Child("check"), assertion, actual, bindings, opts...) if err != nil { return fails, err } if len(checkFails) > 0 { - fail := Result{ - ErrorList: checkFails, - } - if assertion.Message != "" { - fail.Message = template.String(ctx, assertion.Message, actual, bindings, opts...) - } - fails = append(fails, fail) + fails = append(fails, checkFails...) } } return fails, nil