Repository: kyverno/kyverno (Go)

Issue:
[Feature] The ability to sum quantities
### Problem Statement

I would like to write a policy that checks that the combined requests or limits are not greater than the available node resources. The jmesPath `sum` function does not work as it is not quantity aware so it only works with numbers. The additional Kyverno `add` function can only add two values together. I need to be able to add an array of values.

### Solution Description

A function like `add` that is quantity aware but works with a list of values would solve this problem.

### Alternatives

_No response_

### Additional Context

I am happy to contribute to code for this if it is approved.

### Slack discussion

_No response_

### Research

- [X] I have read and followed the documentation AND the [troubleshooting guide](https://kyverno.io/docs/troubleshooting/).
- [X] I have searched other issues in this repository and mine is not recorded.


Discussion:
Thanks for opening your first issue here! Be sure to follow the issue template!

Sounds like the enhancement here is for the `add()` filter to support a single input as an array of quantities. [Docs](https://main.kyverno.io/docs/writing-policies/jmespath/#add) for reference.
@adamjohnson01 we would most certainly welcome a contribution in this area. I agree this is a gap today.
@adamjohnson01 are you working on this? If not, @chipzoller can I take this up?
@XDRAGON2002 I am not working on it. 
Oh, okay, in that case @chipzoller I would like to try and resolve this.
@XDRAGON2002 are you stiil working on this issue? If not, then I would like to work on this one.

@VedRatan Sure go ahead, I've been a bit busy the last few weeks.
@adamjohnson01 I'm writing the documentation for the `sum` feature, but didn't find the best example to include, so can you please provide a practical yaml file example of the use case of this feature? Thanks.
Hello,

I noticed that the `sum` feature in Kyverno requires at least one element in the array: https://github.com/kyverno/kyverno/pull/6552/files#diff-008637ba18accc7bbae7d8d42c30a4ed9e1fb3a87c4d215d9f32e47483bbafa6R791

At the same time, however, looking at the JMESPath specification, it looks like given `[]`, the expected output for `sum(@)` should be 0: https://jmespath.org/specification.html#sum

Might be worth pointing that out in the documentation (if it already is I didn't see it, apologies)
Thanks, good to point out. Could you log a docs issue, please?
I opened https://github.com/kyverno/website/issues/1291 but I'm also wondering whether this is an intended behaviour in Kyverno or an oversight in the code
@AlessandroPomponio Seeing this: https://github.com/kyverno/kyverno/blob/f7a962fd11846ebebf85e33cfaf85dc92f6d117e/pkg/engine/jmespath/functions.go#L858
I think this is an expected behavior and it should be documented. I will verify and add to the docs.



Patch:
diff --git a/pkg/engine/jmespath/functions.go b/pkg/engine/jmespath/functions.go
index c6f01133d82f..5d821af78f85 100644
--- a/pkg/engine/jmespath/functions.go
+++ b/pkg/engine/jmespath/functions.go
@@ -51,6 +51,7 @@ var (
 	labelMatch             = "label_match"
 	toBoolean              = "to_boolean"
 	add                    = "add"
+	sum                    = "sum"
 	subtract               = "subtract"
 	multiply               = "multiply"
 	divide                 = "divide"
@@ -248,6 +249,16 @@ func GetFunctions() []FunctionEntry {
 		},
 		ReturnType: []jpType{jpAny},
 		Note:       "does arithmetic addition of two specified values of numbers, quantities, and durations",
+	}, {
+		FunctionEntry: gojmespath.FunctionEntry{
+			Name: sum,
+			Arguments: []argSpec{
+				{Types: []jpType{jpArray}},
+			},
+			Handler: jpSum,
+		},
+		ReturnType: []jpType{jpAny},
+		Note:       "does arithmetic addition of specified array of values of numbers, quantities, and durations",
 	}, {
 		FunctionEntry: gojmespath.FunctionEntry{
 			Name: subtract,
@@ -771,6 +782,25 @@ func jpAdd(arguments []interface{}) (interface{}, error) {
 	return op1.Add(op2)
 }
 
+func jpSum(arguments []interface{}) (interface{}, error) {
+	items, ok := arguments[0].([]interface{})
+	if !ok {
+		return nil, formatError(typeMismatchError, sum)
+	}
+	if len(items) == 0 {
+		return nil, formatError(genericError, sum, "at least one element in the array is required")
+	}
+	var err error
+	sum := items[0]
+	for _, item := range items[1:] {
+		sum, err = jpAdd([]interface{}{sum, item})
+		if err != nil {
+			return nil, err
+		}
+	}
+	return sum, nil
+}
+
 func jpSubtract(arguments []interface{}) (interface{}, error) {
 	op1, op2, err := ParseArithemticOperands(arguments, subtract)
 	if err != nil {


Test patch:
diff --git a/pkg/engine/jmespath/arithmetic_test.go b/pkg/engine/jmespath/arithmetic_test.go
index b9b4f93ff5bd..03d76a4c3a82 100644
--- a/pkg/engine/jmespath/arithmetic_test.go
+++ b/pkg/engine/jmespath/arithmetic_test.go
@@ -106,6 +106,152 @@ func Test_Add(t *testing.T) {
 	}
 }
 
+func Test_Sum(t *testing.T) {
+	testCases := []struct {
+		name           string
+		test           string
+		expectedResult interface{}
+		err            bool
+		retFloat       bool
+	}{
+		// Scalar
+		{
+			name: "sum([]) -> error",
+			test: "sum([])",
+			err:  true,
+		},
+		{
+			name:           "sum(Scalar[]) -> Scalar",
+			test:           "sum([`12`])",
+			expectedResult: 12.0,
+			retFloat:       true,
+		},
+		{
+			name:           "sum(Scalar[]) -> Scalar",
+			test:           "sum([`12`, `13`, `1`, `4`])",
+			expectedResult: 30.0,
+			retFloat:       true,
+		},
+		{
+			name:           "sum(Scalar[]) -> Scalar",
+			test:           "sum([`12`, `13`])",
+			expectedResult: 25.0,
+			retFloat:       true,
+		},
+		{
+			name: "sum(Scalar[Scalar, Duration, ..]) -> error",
+			test: "sum(['12', '13s'])",
+			err:  true,
+		},
+		{
+			name: "sum(Scalar[Scalar, Quantity, ..]) -> error",
+			test: "sum([`12`, '13Ki'])",
+			err:  true,
+		},
+		{
+			name: "sum(Scalar[Scalar, Quatity, ..]) -> error",
+			test: "sum([`12`, '13'])",
+			err:  true,
+		},
+		// Quantity
+		{
+			name: "sum([]) -> error",
+			test: "sum([])",
+			err:  true,
+		},
+		{
+			name:           "sum(Quantity[]) -> Quantity",
+			test:           "sum(['12Ki'])",
+			expectedResult: `12Ki`,
+		},
+		{
+			name:           "sum(Quantity[]) -> Quantity",
+			test:           "sum(['12Ki', '13Ki', '1Ki', '4Ki'])",
+			expectedResult: `30Ki`,
+		},
+		{
+			name:           "sum(Quantity[]) -> Quantity",
+			test:           "sum(['12Ki', '13Ki'])",
+			expectedResult: `25Ki`,
+		},
+		{
+			name:           "sum(Quantity[]) -> Quantity",
+			test:           "sum(['12Ki', '13'])",
+			expectedResult: `12301`,
+		},
+		{
+			name: "sum(Quantity[Quantity, Duration, ..]) -> error",
+			test: "sum(['12Ki', '13s'])",
+			err:  true,
+		},
+		{
+			name: "sum(Quantity[Quantity, Scalar, ..]) -> error",
+			test: "sum(['12Ki', `13`])",
+			err:  true,
+		},
+		// Duration
+		{
+			name: "sum([]) -> error",
+			test: "sum([])",
+			err:  true,
+		},
+		{
+			name:           "sum(Duration[]) -> Duration",
+			test:           "sum(['12s'])",
+			expectedResult: `12s`,
+		},
+		{
+			name:           "sum(Duration[]) -> Duration",
+			test:           "sum(['12s', '13s', '1s', '4s'])",
+			expectedResult: `30s`,
+		},
+		{
+			name:           "sum(Duration[]) -> Duration",
+			test:           "sum(['12s', '13s'])",
+			expectedResult: `25s`,
+		},
+		{
+			name: "sum(Duration[Duration, Scalar, ..]) -> error",
+			test: "sum(['12s', `13`])",
+			err:  true,
+		},
+		{
+			name: "sum(Duration[Duration, Quantity, ..]) -> error",
+			test: "sum(['12s', '13Ki'])",
+			err:  true,
+		},
+		{
+			name: "sum(Duration[Duration, Quantity, ..]) -> error",
+			test: "sum(['12s', '13'])",
+			err:  true,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			jp, err := New(tc.test)
+			assert.NilError(t, err)
+
+			result, err := jp.Search("")
+			if !tc.err {
+				assert.NilError(t, err)
+			} else {
+				assert.Assert(t, err != nil)
+				return
+			}
+
+			if tc.retFloat {
+				equal, ok := result.(float64)
+				assert.Assert(t, ok)
+				assert.Equal(t, equal, tc.expectedResult.(float64))
+			} else {
+				equal, ok := result.(string)
+				assert.Assert(t, ok)
+				assert.Equal(t, equal, tc.expectedResult.(string))
+			}
+		})
+	}
+}
+
 func Test_Subtract(t *testing.T) {
 	testCases := []struct {
 		name           string