1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-28 02:18:15 +00:00

add semver_compare JMESPath function (#2846)

* add semver_compare JMESPath function

Signed-off-by: Namanl2001 <namanlakhwani@gmail.com>

* adding tests for semver_compare

Signed-off-by: Namanl2001 <namanlakhwani@gmail.com>

* enabling version compaision via regular operators

Signed-off-by: Namanl2001 <namanlakhwani@gmail.com>

* adding tests for version compaision via regular operators

Signed-off-by: Namanl2001 <namanlakhwani@gmail.com>

* removing unnecessary switch cases

Signed-off-by: Namanl2001 <namanlakhwani@gmail.com>

Co-authored-by: Jim Bugwadia <jim@nirmata.com>
This commit is contained in:
Naman Lakhwani 2021-12-21 21:42:35 +05:30 committed by GitHub
parent 6a942683b0
commit 898520b7cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 126 additions and 8 deletions

1
go.mod
View file

@ -55,6 +55,7 @@ require (
require (
github.com/aquilax/truncate v1.0.0 // indirect
github.com/blang/semver/v4 v4.0.0
github.com/opencontainers/image-spec v1.0.2 // indirect
gopkg.in/inf.v0 v0.9.1
)

1
go.sum
View file

@ -276,6 +276,7 @@ github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAw
github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc=
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=

View file

@ -12,6 +12,7 @@ import (
"time"
trunc "github.com/aquilax/truncate"
"github.com/blang/semver/v4"
gojmespath "github.com/jmespath/go-jmespath"
"github.com/minio/pkg/wildcard"
)
@ -55,6 +56,7 @@ var (
timeSince = "time_since"
pathCanonicalize = "path_canonicalize"
truncate = "truncate"
semverCompare = "semver_compare"
)
const errorPrefix = "JMESPath function '%s': "
@ -252,6 +254,14 @@ func getFunctions() []*gojmespath.FunctionEntry {
},
Handler: jpTruncate,
},
{
Name: semverCompare,
Arguments: []ArgSpec{
{Types: []JpType{JpString}},
{Types: []JpType{JpString}},
},
Handler: jpSemverCompare,
},
}
}
@ -631,6 +641,30 @@ func jpTruncate(arguments []interface{}) (interface{}, error) {
return trunc.Truncator(str.String(), int(normalizedLength), trunc.CutStrategy{}), nil
}
func jpSemverCompare(arguments []interface{}) (interface{}, error) {
var err error
v, err := validateArg(semverCompare, arguments, 0, reflect.String)
if err != nil {
return nil, err
}
r, err := validateArg(semverCompare, arguments, 1, reflect.String)
if err != nil {
return nil, err
}
version, _ := semver.Parse(v.String())
expectedRange, err := semver.ParseRange(r.String())
if err != nil {
return nil, err
}
if expectedRange(version) {
return true, nil
}
return false, nil
}
// InterfaceToString casts an interface to a string type
func ifaceToString(iface interface{}) (string, error) {
switch i := iface.(type) {

View file

@ -1023,3 +1023,40 @@ func Test_Truncate(t *testing.T) {
})
}
}
func Test_SemverCompare(t *testing.T) {
testCases := []struct {
jmesPath string
expectedResult bool
}{
{
jmesPath: "semver_compare('4.1.3','>=4.1.x')",
expectedResult: true,
},
{
jmesPath: "semver_compare('4.1.3','!4.x.x')",
expectedResult: false,
},
{
jmesPath: "semver_compare('1.8.6','>1.0.0 <2.0.0')", // >1.0.0 AND <2.0.0
expectedResult: true,
},
{
jmesPath: "semver_compare('2.1.5','<2.0.0 || >=3.0.0')", // <2.0.0 OR >=3.0.0
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.jmesPath, func(t *testing.T) {
jp, err := New(tc.jmesPath)
assert.NilError(t, err)
result, err := jp.Search("")
assert.NilError(t, err)
res, ok := result.(bool)
assert.Assert(t, ok)
assert.Equal(t, res, tc.expectedResult)
})
}
}

View file

@ -51,6 +51,9 @@ func TestEvaluate(t *testing.T) {
{kyverno.Condition{Key: []interface{}{map[string]string{"foo": "bar"}}, Operator: kyverno.Equals, Value: []interface{}{map[string]string{"bar": "foo"}}}, false},
{kyverno.Condition{Key: "1h", Operator: kyverno.Equals, Value: 3600}, true},
{kyverno.Condition{Key: "2h", Operator: kyverno.Equals, Value: 3600}, false},
{kyverno.Condition{Key: "1.5.2", Operator: kyverno.Equals, Value: "1.5.2"}, true},
{kyverno.Condition{Key: "1.5.2", Operator: kyverno.Equals, Value: "1.5.*"}, true},
{kyverno.Condition{Key: "1.5.0", Operator: kyverno.Equals, Value: "1.5.5"}, false},
// Not Equals
{kyverno.Condition{Key: "string", Operator: kyverno.NotEquals, Value: "string"}, false},
@ -88,6 +91,9 @@ func TestEvaluate(t *testing.T) {
{kyverno.Condition{Key: []interface{}{map[string]string{"foo": "bar"}}, Operator: kyverno.NotEquals, Value: []interface{}{map[string]string{"bar": "foo"}}}, true},
{kyverno.Condition{Key: "1h", Operator: kyverno.NotEquals, Value: 3600}, false},
{kyverno.Condition{Key: "2h", Operator: kyverno.NotEquals, Value: 3600}, true},
{kyverno.Condition{Key: "1.5.2", Operator: kyverno.NotEquals, Value: "1.5.5"}, true},
{kyverno.Condition{Key: "1.5.2", Operator: kyverno.NotEquals, Value: "1.5.*"}, false},
{kyverno.Condition{Key: "1.5.0", Operator: kyverno.NotEquals, Value: "1.5.0"}, false},
// Greater Than
{kyverno.Condition{Key: 10, Operator: kyverno.GreaterThan, Value: 1}, true},
@ -129,6 +135,8 @@ func TestEvaluate(t *testing.T) {
{kyverno.Condition{Key: -5, Operator: kyverno.GreaterThan, Value: 1}, false},
{kyverno.Condition{Key: -5, Operator: kyverno.GreaterThan, Value: -10}, true},
{kyverno.Condition{Key: 1, Operator: kyverno.GreaterThan, Value: -10}, true},
{kyverno.Condition{Key: "1.5.5", Operator: kyverno.GreaterThan, Value: "1.5.0"}, true},
{kyverno.Condition{Key: "1.5.0", Operator: kyverno.GreaterThan, Value: "1.5.5"}, false},
// Less Than
{kyverno.Condition{Key: 10, Operator: kyverno.LessThan, Value: 1}, false},
@ -170,6 +178,8 @@ func TestEvaluate(t *testing.T) {
{kyverno.Condition{Key: -5, Operator: kyverno.LessThan, Value: 1}, true},
{kyverno.Condition{Key: -5, Operator: kyverno.LessThan, Value: -10}, false},
{kyverno.Condition{Key: 1, Operator: kyverno.LessThan, Value: -10}, false},
{kyverno.Condition{Key: "1.5.5", Operator: kyverno.LessThan, Value: "1.5.0"}, false},
{kyverno.Condition{Key: "1.5.0", Operator: kyverno.LessThan, Value: "1.5.5"}, true},
// Greater Than or Equal
{kyverno.Condition{Key: 10, Operator: kyverno.GreaterThanOrEquals, Value: 1}, true},
@ -206,6 +216,9 @@ func TestEvaluate(t *testing.T) {
{kyverno.Condition{Key: 1, Operator: kyverno.GreaterThanOrEquals, Value: int64(1)}, true},
{kyverno.Condition{Key: 10, Operator: kyverno.GreaterThanOrEquals, Value: int64(1)}, true},
{kyverno.Condition{Key: 1, Operator: kyverno.GreaterThanOrEquals, Value: int64(10)}, false},
{kyverno.Condition{Key: "1.5.5", Operator: kyverno.GreaterThanOrEquals, Value: "1.5.5"}, true},
{kyverno.Condition{Key: "1.5.5", Operator: kyverno.GreaterThanOrEquals, Value: "1.5.0"}, true},
{kyverno.Condition{Key: "1.5.0", Operator: kyverno.GreaterThanOrEquals, Value: "1.5.5"}, false},
// Less Than or Equal
{kyverno.Condition{Key: 10, Operator: kyverno.LessThanOrEquals, Value: 1}, false},
@ -242,6 +255,9 @@ func TestEvaluate(t *testing.T) {
{kyverno.Condition{Key: 1, Operator: kyverno.LessThanOrEquals, Value: int64(1)}, true},
{kyverno.Condition{Key: 10, Operator: kyverno.LessThanOrEquals, Value: int64(1)}, false},
{kyverno.Condition{Key: 1, Operator: kyverno.LessThanOrEquals, Value: int64(10)}, true},
{kyverno.Condition{Key: "1.5.5", Operator: kyverno.LessThanOrEquals, Value: "1.5.5"}, true},
{kyverno.Condition{Key: "1.5.0", Operator: kyverno.LessThanOrEquals, Value: "1.5.5"}, true},
{kyverno.Condition{Key: "1.5.5", Operator: kyverno.LessThanOrEquals, Value: "1.5.0"}, false},
// In
{kyverno.Condition{Key: 1, Operator: kyverno.In, Value: []interface{}{1, 2, 3}}, true},

View file

@ -4,6 +4,7 @@ import (
"fmt"
"strconv"
"github.com/blang/semver/v4"
"github.com/go-logr/logr"
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine/context"
@ -37,14 +38,22 @@ func compareByCondition(key float64, value float64, op kyverno.ConditionOperator
return key <= value
case kyverno.LessThan:
return key < value
case kyverno.Equals:
return key == value
case kyverno.Equal:
return key == value
case kyverno.NotEquals:
return key != value
case kyverno.NotEqual:
return key != value
default:
(*log).Info(fmt.Sprintf("Expected operator, one of [GreaterThanOrEquals, GreaterThan, LessThanOrEquals, LessThan, Equals, NotEquals], found %s", op))
return false
}
}
func compareVersionByCondition(key semver.Version, value semver.Version, op kyverno.ConditionOperator, log *logr.Logger) bool {
switch op {
case kyverno.GreaterThanOrEquals:
return key.GTE(value)
case kyverno.GreaterThan:
return key.GT(value)
case kyverno.LessThanOrEquals:
return key.LTE(value)
case kyverno.LessThan:
return key.LT(value)
default:
(*log).Info(fmt.Sprintf("Expected operator, one of [GreaterThanOrEquals, GreaterThan, LessThanOrEquals, LessThan, Equals, NotEquals], found %s", op))
return false
@ -141,6 +150,21 @@ func (noh NumericOperatorHandler) validateValueWithResourcePattern(key resource.
}
}
func (noh NumericOperatorHandler) validateValueWithVersionPattern(key semver.Version, value interface{}) bool {
switch typedValue := value.(type) {
case string:
versionValue, err := semver.Parse(typedValue)
if err != nil {
noh.log.Error(fmt.Errorf("parse error: "), "Failed to parse value type doesn't match key type")
return false
}
return compareVersionByCondition(key, versionValue, noh.condition, &noh.log)
default:
noh.log.Info("Expected type string", "value", value, "type", fmt.Sprintf("%T", value))
return false
}
}
func (noh NumericOperatorHandler) validateValueWithStringPattern(key string, value interface{}) bool {
// We need to check duration first as it's the only type that can be compared to a different type
durationKey, durationValue, err := parseDuration(key, value)
@ -162,6 +186,11 @@ func (noh NumericOperatorHandler) validateValueWithStringPattern(key string, val
if err == nil {
return noh.validateValueWithResourcePattern(resourceKey, value)
}
// attempt to extract version from string
versionKey, err := semver.Parse(key)
if err == nil {
return noh.validateValueWithVersionPattern(versionKey, value)
}
noh.log.Error(err, "Failed to parse from the string key, value is not float, int nor resource quantity")
return false