From b02d12ff1e007245e504348c25152d2d45051e3d Mon Sep 17 00:00:00 2001 From: David Morgan <105383926+david-a-morgan@users.noreply.github.com> Date: Mon, 10 Jul 2023 13:02:00 -0400 Subject: [PATCH] JSONPath support for json compare analyzer (#1244) This adds JSONPath support to the json compare analyzer using k8s.io/client-go/util/jsonpath implementation. To preserve backwards compatibility a new attribute, `JsonPath, is added to the compare analyzer as opposed to changing how `Path` works. Only one should be set, but preference is given to `Path`, again to maintain backwards compatibility. As a convience for users, if the result of running the JSONPath expression returns a single value, that value is unwrapped from its enclosing array and used as the comparison with `Value`. This isn't strictly compatible with how JSONPath works (all results are wrapped in an array), but it's easier for end users who are expecting a single result from their JSONPath expression. --- config/crds/troubleshoot.sh_analyzers.yaml | 2 + config/crds/troubleshoot.sh_preflights.yaml | 2 + .../crds/troubleshoot.sh_supportbundles.yaml | 2 + pkg/analyze/json_compare.go | 28 ++++ pkg/analyze/json_compare_test.go | 138 ++++++++++++++++++ .../troubleshoot/v1beta2/analyzer_shared.go | 1 + schemas/analyzer-troubleshoot-v1beta2.json | 3 + schemas/preflight-troubleshoot-v1beta2.json | 3 + .../supportbundle-troubleshoot-v1beta2.json | 3 + 9 files changed, 182 insertions(+) diff --git a/config/crds/troubleshoot.sh_analyzers.yaml b/config/crds/troubleshoot.sh_analyzers.yaml index c0425b7a..7256fe2c 100644 --- a/config/crds/troubleshoot.sh_analyzers.yaml +++ b/config/crds/troubleshoot.sh_analyzers.yaml @@ -732,6 +732,8 @@ spec: type: BoolString fileName: type: string + jsonPath: + type: string outcomes: items: properties: diff --git a/config/crds/troubleshoot.sh_preflights.yaml b/config/crds/troubleshoot.sh_preflights.yaml index ac66e104..c9dbcfc1 100644 --- a/config/crds/troubleshoot.sh_preflights.yaml +++ b/config/crds/troubleshoot.sh_preflights.yaml @@ -732,6 +732,8 @@ spec: type: BoolString fileName: type: string + jsonPath: + type: string outcomes: items: properties: diff --git a/config/crds/troubleshoot.sh_supportbundles.yaml b/config/crds/troubleshoot.sh_supportbundles.yaml index 50273871..c3d94aa5 100644 --- a/config/crds/troubleshoot.sh_supportbundles.yaml +++ b/config/crds/troubleshoot.sh_supportbundles.yaml @@ -763,6 +763,8 @@ spec: type: BoolString fileName: type: string + jsonPath: + type: string outcomes: items: properties: diff --git a/pkg/analyze/json_compare.go b/pkg/analyze/json_compare.go index 242d8cfe..4e60b44b 100644 --- a/pkg/analyze/json_compare.go +++ b/pkg/analyze/json_compare.go @@ -1,6 +1,7 @@ package analyzer import ( + "bytes" "encoding/json" "path/filepath" "reflect" @@ -9,6 +10,7 @@ import ( "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" iutils "github.com/replicatedhq/troubleshoot/pkg/interfaceutils" + "k8s.io/client-go/util/jsonpath" ) type AnalyzeJsonCompare struct { @@ -55,6 +57,32 @@ func (a *AnalyzeJsonCompare) analyzeJsonCompare(analyzer *troubleshootv1beta2.Js if err != nil { return nil, errors.Wrapf(err, "failed to get object at path: %s", analyzer.Path) } + } else if analyzer.JsonPath != "" { + jsp := jsonpath.New(analyzer.CheckName) + jsp.AllowMissingKeys(true).EnableJSONOutput(true) + err = jsp.Parse(analyzer.JsonPath) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse jsonpath: %s", analyzer.JsonPath) + } + + var data bytes.Buffer + err = jsp.Execute(&data, actual) + if err != nil { + return nil, errors.Wrap(err, "failed to execute jsonpath") + } + + err = json.NewDecoder(&data).Decode(&actual) + if err != nil { + return nil, errors.Wrap(err, "failed to decode jsonpath result") + } + + // If we get back a single result in a slice unwrap it. + // Technically this doesn't strictly follow jsonpath, but it makes + // things easier downstream. Basically we don't want to require + // users to wrap a single result with []. + if a, ok := actual.([]interface{}); ok && len(a) == 1 { + actual = a[0] + } } var expected interface{} diff --git a/pkg/analyze/json_compare_test.go b/pkg/analyze/json_compare_test.go index 5c7d6f77..be272547 100644 --- a/pkg/analyze/json_compare_test.go +++ b/pkg/analyze/json_compare_test.go @@ -541,6 +541,144 @@ func Test_jsonCompare(t *testing.T) { }, fileContents: []byte(``), }, + { + name: "jsonpath comparison", + analyzer: troubleshootv1beta2.JsonCompare{ + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + Message: "pass", + }, + }, + { + Fail: &troubleshootv1beta2.SingleOutcome{ + Message: "fail", + }, + }, + }, + CollectorName: "jsonpath-compare-1", + FileName: "jsonpath-compare-1.json", + JsonPath: "{$.morestuff[0]}", + Value: `{ + "foo": { + "bar": 123 + } + }`, + }, + expectResult: AnalyzeResult{ + IsPass: true, + IsWarn: false, + IsFail: false, + Title: "jsonpath-compare-1", + Message: "pass", + IconKey: "kubernetes_text_analyze", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg", + }, + fileContents: []byte(`{ + "foo": "bar", + "stuff": { + "foo": "bar", + "bar": true + }, + "morestuff": [ + { + "foo": { + "bar": 123 + } + } + ] + }`), + }, + { + name: "jsonpath comparison, but fail on match", + analyzer: troubleshootv1beta2.JsonCompare{ + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + Message: "pass", + When: "false", + }, + }, + { + Fail: &troubleshootv1beta2.SingleOutcome{ + Message: "fail", + When: "true", + }, + }, + }, + CollectorName: "jsonpath-compare-1-1", + FileName: "jsonpath-compare-1-1.json", + JsonPath: "{$.morestuff[0].foo.bar}", + Value: `123`, + }, + expectResult: AnalyzeResult{ + IsPass: false, + IsWarn: false, + IsFail: true, + Title: "jsonpath-compare-1-1", + Message: "fail", + IconKey: "kubernetes_text_analyze", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg", + }, + fileContents: []byte(`{ + "foo": "bar", + "stuff": { + "foo": "bar", + "bar": true + }, + "morestuff": [ + { + "foo": { + "bar": 123 + } + } + ] + }`), + }, + { + name: "jsonpath comparison, multiple values", + analyzer: troubleshootv1beta2.JsonCompare{ + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + Message: "pass", + }, + }, + { + Fail: &troubleshootv1beta2.SingleOutcome{ + Message: "fail", + }, + }, + }, + CollectorName: "jsonpath-compare-2", + FileName: "jsonpath-compare-2.json", + JsonPath: "{$..bar}", + Value: `[true, 123]`, + }, + expectResult: AnalyzeResult{ + IsPass: true, + IsWarn: false, + IsFail: false, + Title: "jsonpath-compare-2", + Message: "pass", + IconKey: "kubernetes_text_analyze", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg", + }, + fileContents: []byte(`{ + "foo": "bar", + "stuff": { + "foo": "bar", + "bar": true + }, + "morestuff": [ + { + "foo": { + "bar": 123 + } + } + ] + }`), + }, } for _, test := range tests { diff --git a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go index 1a9278de..4cb171d3 100644 --- a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go @@ -161,6 +161,7 @@ type JsonCompare struct { CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"` FileName string `json:"fileName,omitempty" yaml:"fileName,omitempty"` Path string `json:"path,omitempty" yaml:"path,omitempty"` + JsonPath string `json:"jsonPath,omitempty" yaml:"jsonPath,omitempty"` Value string `json:"value,omitempty" yaml:"value,omitempty"` Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` } diff --git a/schemas/analyzer-troubleshoot-v1beta2.json b/schemas/analyzer-troubleshoot-v1beta2.json index b934fd2d..1f1eaff9 100644 --- a/schemas/analyzer-troubleshoot-v1beta2.json +++ b/schemas/analyzer-troubleshoot-v1beta2.json @@ -1092,6 +1092,9 @@ "fileName": { "type": "string" }, + "jsonPath": { + "type": "string" + }, "outcomes": { "type": "array", "items": { diff --git a/schemas/preflight-troubleshoot-v1beta2.json b/schemas/preflight-troubleshoot-v1beta2.json index bf6d466d..8074b71f 100644 --- a/schemas/preflight-troubleshoot-v1beta2.json +++ b/schemas/preflight-troubleshoot-v1beta2.json @@ -1092,6 +1092,9 @@ "fileName": { "type": "string" }, + "jsonPath": { + "type": "string" + }, "outcomes": { "type": "array", "items": { diff --git a/schemas/supportbundle-troubleshoot-v1beta2.json b/schemas/supportbundle-troubleshoot-v1beta2.json index de206ffa..436b4a8d 100644 --- a/schemas/supportbundle-troubleshoot-v1beta2.json +++ b/schemas/supportbundle-troubleshoot-v1beta2.json @@ -1138,6 +1138,9 @@ "fileName": { "type": "string" }, + "jsonPath": { + "type": "string" + }, "outcomes": { "type": "array", "items": {