diff --git a/pkg/engine/jmespath/functions.go b/pkg/engine/jmespath/functions.go index 07ba8459b8..1f73a314b4 100644 --- a/pkg/engine/jmespath/functions.go +++ b/pkg/engine/jmespath/functions.go @@ -16,6 +16,7 @@ import ( "github.com/blang/semver/v4" gojmespath "github.com/jmespath/go-jmespath" "github.com/minio/pkg/wildcard" + "sigs.k8s.io/yaml" ) var ( @@ -59,6 +60,7 @@ var ( truncate = "truncate" semverCompare = "semver_compare" parseJson = "parse_json" + parseYAML = "parse_yaml" ) const errorPrefix = "JMESPath function '%s': " @@ -271,6 +273,13 @@ func getFunctions() []*gojmespath.FunctionEntry { }, Handler: jpParseJson, }, + { + Name: parseYAML, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + }, + Handler: jpParseYAML, + }, } } @@ -684,6 +693,20 @@ func jpParseJson(arguments []interface{}) (interface{}, error) { return output, err } +func jpParseYAML(arguments []interface{}) (interface{}, error) { + input, err := validateArg(parseYAML, arguments, 0, reflect.String) + if err != nil { + return nil, err + } + jsonData, err := yaml.YAMLToJSON([]byte(input.String())) + if err != nil { + return nil, err + } + var output interface{} + err = json.Unmarshal(jsonData, &output) + return output, err +} + // 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 e9786259b5..ec26422411 100644 --- a/pkg/engine/jmespath/functions_test.go +++ b/pkg/engine/jmespath/functions_test.go @@ -101,6 +101,82 @@ func Test_ParseJsonComplex(t *testing.T) { } } +func Test_ParseYAML(t *testing.T) { + testCases := []struct { + input string + output interface{} + }{ + { + input: `a: b`, + output: map[string]interface{}{ + "a": "b", + }, + }, + { + input: ` +- 1 +- 2 +- 3 +- a: b`, + output: []interface{}{ + 1.0, + 2.0, + 3.0, + map[string]interface{}{ + "a": "b", + }, + }, + }, + { + input: ` +spec: + test: 1 + test2: + - 2 + - 3 +`, + output: map[string]interface{}{ + "spec": map[string]interface{}{ + "test": 1.0, + "test2": []interface{}{2.0, 3.0}, + }, + }, + }, + { + input: ` +bar: > + this is not a normal string it + spans more than + one line + see?`, + output: map[string]interface{}{ + "bar": "this is not a normal string it spans more than one line see?", + }, + }, + { + input: ` +--- +foo: ~ +bar: null +`, + output: map[string]interface{}{ + "bar": nil, + "foo": nil, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.input, func(t *testing.T) { + jp, err := New(fmt.Sprintf(`parse_yaml('%s')`, tc.input)) + assert.NilError(t, err) + result, err := jp.Search("") + assert.NilError(t, err) + assert.DeepEqual(t, result, tc.output) + }) + } +} + func Test_EqualFold(t *testing.T) { testCases := []struct { jmesPath string diff --git a/test/cli/test/custom-functions/kyverno-test.yaml b/test/cli/test/custom-functions/kyverno-test.yaml index 173dd91d84..bfd29bdafa 100644 --- a/test/cli/test/custom-functions/kyverno-test.yaml +++ b/test/cli/test/custom-functions/kyverno-test.yaml @@ -39,3 +39,23 @@ results: resource: invalid-test kind: ConfigMap result: fail + - policy: test-parse-yaml + rule: test-yaml-parsing-jmespath + resource: valid-yaml-test + kind: ConfigMap + result: pass + - policy: test-parse-yaml + rule: test-yaml-parsing-jmespath + resource: invalid-yaml-test + kind: ConfigMap + result: fail + - policy: test-parse-yaml-array + rule: test-yaml-parsing-jmespath + resource: valid-yaml-test + kind: ConfigMap + result: pass + - policy: test-parse-yaml-array + rule: test-yaml-parsing-jmespath + resource: invalid-yaml-test + kind: ConfigMap + result: fail diff --git a/test/cli/test/custom-functions/policy.yaml b/test/cli/test/custom-functions/policy.yaml index cb4cb30cf3..cf4fe99b2b 100644 --- a/test/cli/test/custom-functions/policy.yaml +++ b/test/cli/test/custom-functions/policy.yaml @@ -95,3 +95,45 @@ spec: - key: "{{request.object.metadata.annotations.test | parse_json(@).a }}" operator: NotEquals value: b +--- +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: test-parse-yaml +spec: + validationFailureAction: enforce + background: false + rules: + - name: test-yaml-parsing-jmespath + match: + resources: + kinds: + - ConfigMap + validate: + message: "Test JMESPath" + deny: + conditions: + - key: "{{request.object.metadata.annotations.test | parse_yaml(@).value }}" + operator: NotEquals + value: "a" +--- +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: test-parse-yaml-array +spec: + validationFailureAction: enforce + background: false + rules: + - name: test-yaml-parsing-jmespath + match: + resources: + kinds: + - ConfigMap + validate: + message: "Test JMESPath" + deny: + conditions: + - key: a + operator: NotIn + value: "{{request.object.metadata.annotations.test | parse_yaml(@).array }}" diff --git a/test/cli/test/custom-functions/resources.yaml b/test/cli/test/custom-functions/resources.yaml index f9bddf3824..347dab8835 100644 --- a/test/cli/test/custom-functions/resources.yaml +++ b/test/cli/test/custom-functions/resources.yaml @@ -67,3 +67,28 @@ metadata: annotations: test: | {"a": "not-b"} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: valid-yaml-test + annotations: + test: | + value: a + array: + - a + - b + - c +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: invalid-yaml-test + annotations: + test: | + value: b + array: + - d + - e + - f +