diff --git a/go.mod b/go.mod index fa376b15ea..3a1b2d8a57 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 70c42b1d47..422fddf196 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/engine/jmespath/functions.go b/pkg/engine/jmespath/functions.go index 801280e18d..8488f48eb7 100644 --- a/pkg/engine/jmespath/functions.go +++ b/pkg/engine/jmespath/functions.go @@ -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) { diff --git a/pkg/engine/jmespath/functions_test.go b/pkg/engine/jmespath/functions_test.go index b431595afe..8dbbbaa9c1 100644 --- a/pkg/engine/jmespath/functions_test.go +++ b/pkg/engine/jmespath/functions_test.go @@ -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) + }) + } +} diff --git a/pkg/engine/variables/evaluate_test.go b/pkg/engine/variables/evaluate_test.go index 4b600cab15..aa16670f6f 100644 --- a/pkg/engine/variables/evaluate_test.go +++ b/pkg/engine/variables/evaluate_test.go @@ -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}, diff --git a/pkg/engine/variables/operator/numeric.go b/pkg/engine/variables/operator/numeric.go index 36e16fc996..2a505a62fa 100644 --- a/pkg/engine/variables/operator/numeric.go +++ b/pkg/engine/variables/operator/numeric.go @@ -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