From af51ceb4ffb5d5f560bf2262c0dad57d89985e03 Mon Sep 17 00:00:00 2001
From: gsweene2 <gsweene2@u.rochester.edu>
Date: Wed, 4 May 2022 06:33:24 -0400
Subject: [PATCH] Add JMESPath Function `items` (#3777)

Co-authored-by: Jim Bugwadia <jim@nirmata.com>
Co-authored-by: Sambhav Kothari <sambhavs.email@gmail.com>
Co-authored-by: Sambhav Kothari <skothari44@bloomberg.net>
---
 pkg/engine/jmespath/functions.go              | 49 ++++++++++++++++++
 pkg/engine/jmespath/functions_test.go         | 50 +++++++++++++++++++
 .../test/context-entries/kyverno-test.yaml    |  5 ++
 test/cli/test/context-entries/policies.yaml   | 31 ++++++++++--
 4 files changed, 132 insertions(+), 3 deletions(-)

diff --git a/pkg/engine/jmespath/functions.go b/pkg/engine/jmespath/functions.go
index 43d7ad3216..16dd46f121 100644
--- a/pkg/engine/jmespath/functions.go
+++ b/pkg/engine/jmespath/functions.go
@@ -8,6 +8,7 @@ import (
 	"path/filepath"
 	"reflect"
 	"regexp"
+	"sort"
 	"strconv"
 	"strings"
 	"time"
@@ -62,6 +63,7 @@ var (
 	semverCompare          = "semver_compare"
 	parseJson              = "parse_json"
 	parseYAML              = "parse_yaml"
+	items                  = "items"
 )
 
 const errorPrefix = "JMESPath function '%s': "
@@ -368,6 +370,18 @@ func GetFunctions() []*FunctionEntry {
 			ReturnType: []JpType{JpAny},
 			Note:       "decodes a valid YAML encoded string to the appropriate type provided it can be represented as JSON",
 		},
+		{
+			Entry: &gojmespath.FunctionEntry{Name: items,
+				Arguments: []ArgSpec{
+					{Types: []JpType{JpObject}},
+					{Types: []JpType{JpString}},
+					{Types: []JpType{JpString}},
+				},
+				Handler: jpItems,
+			},
+			ReturnType: []JpType{JpArray},
+			Note:       "converts a map to an array of objects where each key:value is an item in the array",
+		},
 	}
 
 }
@@ -795,6 +809,41 @@ func jpParseYAML(arguments []interface{}) (interface{}, error) {
 	return output, err
 }
 
+func jpItems(arguments []interface{}) (interface{}, error) {
+	input, ok := arguments[0].(map[string]interface{})
+	if !ok {
+		return nil, fmt.Errorf(invalidArgumentTypeError, arguments, 0, "Object")
+	}
+	keyName, ok := arguments[1].(string)
+	if !ok {
+		return nil, fmt.Errorf(invalidArgumentTypeError, arguments, 1, "String")
+	}
+	valName, ok := arguments[2].(string)
+	if !ok {
+		return nil, fmt.Errorf(invalidArgumentTypeError, arguments, 2, "String")
+	}
+
+	arrayOfObj := make([]map[string]interface{}, 0)
+
+	keys := []string{}
+
+	// Sort the keys so that the output is deterministic
+	for key := range input {
+		keys = append(keys, key)
+	}
+
+	sort.Strings(keys)
+
+	for _, key := range keys {
+		m := make(map[string]interface{})
+		m[keyName] = key
+		m[valName] = input[key]
+		arrayOfObj = append(arrayOfObj, m)
+	}
+
+	return arrayOfObj, 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 0f745290f3..5b421827cf 100644
--- a/pkg/engine/jmespath/functions_test.go
+++ b/pkg/engine/jmespath/functions_test.go
@@ -1284,3 +1284,53 @@ func Test_SemverCompare(t *testing.T) {
 		})
 	}
 }
+
+func Test_Items(t *testing.T) {
+
+	testCases := []struct {
+		object         string
+		keyName        string
+		valName        string
+		expectedResult string
+	}{
+		{
+			object:         `{ "key1": "value1" }`,
+			keyName:        `"key"`,
+			valName:        `"value"`,
+			expectedResult: `[{ "key": "key1", "value": "value1" }]`,
+		},
+		{
+			object:         `{ "key1": "value1", "key2": "value2" }`,
+			keyName:        `"key"`,
+			valName:        `"value"`,
+			expectedResult: `[{ "key": "key1", "value": "value1" }, { "key": "key2", "value": "value2" }]`,
+		},
+		{
+			object:         `{ "key1": "value1", "key2": "value2" }`,
+			keyName:        `"myKey"`,
+			valName:        `"myValue"`,
+			expectedResult: `[{ "myKey": "key1", "myValue": "value1" }, { "myKey": "key2", "myValue": "value2" }]`,
+		},
+	}
+
+	for i, tc := range testCases {
+		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
+
+			query, err := New("items(`" + tc.object + "`,`" + tc.keyName + "`,`" + tc.valName + "`)")
+			assert.NilError(t, err)
+
+			res, err := query.Search("")
+			assert.NilError(t, err)
+
+			result, ok := res.([]map[string]interface{})
+			assert.Assert(t, ok)
+
+			var resource []map[string]interface{}
+			err = json.Unmarshal([]byte(tc.expectedResult), &resource)
+			assert.NilError(t, err)
+
+			assert.DeepEqual(t, result, resource)
+		})
+	}
+
+}
diff --git a/test/cli/test/context-entries/kyverno-test.yaml b/test/cli/test/context-entries/kyverno-test.yaml
index a2463de519..a8158d8fe5 100644
--- a/test/cli/test/context-entries/kyverno-test.yaml
+++ b/test/cli/test/context-entries/kyverno-test.yaml
@@ -49,3 +49,8 @@ results:
     resource: example
     kind: Pod
     result: pass
+  - policy: example
+    rule:  items
+    resource: example
+    kind: Pod
+    result: pass
diff --git a/test/cli/test/context-entries/policies.yaml b/test/cli/test/context-entries/policies.yaml
index 6c95d44497..d88d10900f 100644
--- a/test/cli/test/context-entries/policies.yaml
+++ b/test/cli/test/context-entries/policies.yaml
@@ -126,11 +126,11 @@ spec:
     context:
     - name: obj
       variable:
-        value: 
+        value:
           notName: not-example
     - name: obj
       variable:
-        value: 
+        value:
           name: example
     match:
       resources:
@@ -150,7 +150,7 @@ spec:
     context:
     - name: obj
       variable:
-        value: 
+        value:
          - A=ATest
          - B=BTest
     match:
@@ -163,3 +163,28 @@ spec:
           - key: "A=*"
             operator: AnyNotIn
             value: "{{ obj }}"
+  - name: items
+    context:
+    - name: obj
+      variable:
+        value:
+          a: 1
+          b: 2
+        jmesPath: items(@, 'key', 'value')
+    - name: expected
+      variable:
+        value:
+          - key: a
+            value: 1
+          - key: b
+            value: 2
+    match:
+      resources:
+        kinds:
+        - Pod
+    validate:
+      deny:
+        conditions:
+          - key: "{{ obj }}"
+            operator: NotEqual
+            value: "{{ expected }}"