diff --git a/internal/controller/util/json_util.go b/internal/controller/util/json_util.go index edf9f93c7..507e78b5e 100644 --- a/internal/controller/util/json_util.go +++ b/internal/controller/util/json_util.go @@ -39,7 +39,7 @@ func EvaluateCheckHook(client client.Client, hook *kubeobjects.HookSpec, log log return false, err } - return evaluateCheckHookExp(hook.Chk.Condition, resource) + return EvaluateCheckHookExp(hook.Chk.Condition, resource) case "deployment": // handle deployment type resource := &appsv1.Deployment{} @@ -49,7 +49,7 @@ func EvaluateCheckHook(client client.Client, hook *kubeobjects.HookSpec, log log return false, err } - return evaluateCheckHookExp(hook.Chk.Condition, resource) + return EvaluateCheckHookExp(hook.Chk.Condition, resource) case "statefulset": // handle statefulset type resource := &appsv1.StatefulSet{} @@ -59,7 +59,7 @@ func EvaluateCheckHook(client client.Client, hook *kubeobjects.HookSpec, log log return false, err } - return evaluateCheckHookExp(hook.Chk.Condition, resource) + return EvaluateCheckHookExp(hook.Chk.Condition, resource) } return false, nil @@ -104,7 +104,7 @@ func getTimeoutValue(hook *kubeobjects.HookSpec) int { return defaultTimeoutValue } -func evaluateCheckHookExp(booleanExpression string, jsonData interface{}) (bool, error) { +func EvaluateCheckHookExp(booleanExpression string, jsonData interface{}) (bool, error) { op, jsonPaths, err := parseBooleanExpression(booleanExpression) if err != nil { return false, fmt.Errorf("failed to parse boolean expression: %w", err) @@ -288,8 +288,8 @@ func parseBooleanExpression(booleanExpression string) (op string, jsonPaths []st jsonPaths = trimLeadingTrailingWhiteSpace(exprs) if len(exprs) == 2 && - isValidJSONPathExpression(jsonPaths[0]) && - isValidJSONPathExpression(jsonPaths[1]) { + IsValidJSONPathExpression(jsonPaths[0]) && + IsValidJSONPathExpression(jsonPaths[1]) { return op, jsonPaths, nil } } @@ -297,7 +297,7 @@ func parseBooleanExpression(booleanExpression string) (op string, jsonPaths []st return "", []string{}, fmt.Errorf("unable to parse boolean expression %v", booleanExpression) } -func isValidJSONPathExpression(expr string) bool { +func IsValidJSONPathExpression(expr string) bool { jp := jsonpath.New("validator").AllowMissingKeys(true) err := jp.Parse(expr) diff --git a/internal/controller/util/json_util_test.go b/internal/controller/util/json_util_test.go new file mode 100644 index 000000000..3a481d361 --- /dev/null +++ b/internal/controller/util/json_util_test.go @@ -0,0 +1,204 @@ +package util_test + +import ( + "encoding/json" + "strconv" + "testing" + + "github.com/ramendr/ramen/internal/controller/util" +) + +type testCases struct { + jsonPathExprs string + result bool +} + +var jsonText1 = []byte(`{ + "kind": "Deployment", + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 1, + "revisionHistoryLimit": 10 + }, + "status": { + "replicas": 1, + "conditions": [ + { + "status": "True", + "type": "Progressing" + }, + { + "status": "True", + "type": "Available" + } + ] + } + }`) + +var testCasesData = []testCases{ + { + jsonPathExprs: "{$.status.conditions[0].status} == True", + result: true, + }, + { + jsonPathExprs: "{$.spec.replicas} == 1", + result: true, + }, + { + jsonPathExprs: "{$.status.conditions[0].status} == {True}", + result: false, + }, +} + +func TestXYZ(t *testing.T) { + for i, tt := range testCasesData { + test := tt + + t.Run(strconv.Itoa(i), func(t *testing.T) { + var jsonData map[string]interface{} + err := json.Unmarshal(jsonText1, &jsonData) + if err != nil { + t.Error(err) + } + _, err = util.EvaluateCheckHookExp(test.jsonPathExprs, jsonData) + if (err == nil) != test.result { + t.Errorf("EvaluateCheckHookExp() = %v, want %v", err, test.result) + } + }) + } +} + +func Test_isValidJsonPathExpression(t *testing.T) { + type args struct { + expr string + } + + tests := []struct { + name string + args args + want bool + }{ + { + name: "Simple expression", + args: args{ + expr: "$.spec.replicas", + }, + want: true, + }, + { + name: "no $ at the start", + args: args{ + expr: "{.spec.replicas}", + }, + want: true, + }, + { + name: "element in array", + args: args{ + expr: "{$.status.conditions[0].status}", + }, + want: true, + }, + { + name: "spec 1", + args: args{ + expr: "{$.status.readyReplicas}", + }, + want: true, + }, + { + name: "spec 2", + args: args{ + expr: "{$.status.containerStatuses[0].ready}", + }, + want: true, + }, + { + name: "spec 3a", + args: args{ + expr: "{True}", + }, + want: false, + }, + { + name: "spec 3b", + args: args{ + expr: "{False}", + }, + want: false, + }, + { + name: "spec 3c", + args: args{ + expr: "{true}", + }, + want: true, + }, + { + name: "spec 3d", + args: args{ + expr: "{false}", + }, + want: true, + }, + { + name: "Spec 4", + args: args{ + expr: "{$.spec.replicas}", + }, + want: true, + }, + { + name: "expression with == operator", + args: args{ + expr: "$.store.book[?(@.price > 10)].title==$.store.book[0].title", + }, + want: true, + }, + { + name: "expression with > operator", + args: args{ + expr: "$.store.book[?(@.author CONTAINS 'Smith')].price>20", + }, + want: true, + }, + { + name: "expression with >= operator", + args: args{ + expr: "$.user.age>=$.minimum.age", + }, + want: true, + }, + { + name: "expression with < operator", + args: args{ + expr: "$.user.age<$.maximum.age", + }, + want: true, + }, + { + name: "expression with <= operator", + args: args{ + expr: "$.user.age<=$.maximum.age", + }, + want: true, + }, + { + name: "expression with != operator", + args: args{ + expr: "$.user.age!=$.maximum.age", + }, + want: true, + }, + } + + for _, tt := range tests { + test := tt + + t.Run(test.name, func(t *testing.T) { + if got := util.IsValidJSONPathExpression(test.args.expr); got != test.want { + t.Errorf("IsValidJSONPathExpression() = %v, want %v", got, test.want) + } + }) + } +}