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.
This commit is contained in:
David Morgan
2023-07-10 13:02:00 -04:00
committed by GitHub
parent 24822b7f95
commit b02d12ff1e
9 changed files with 182 additions and 0 deletions

View File

@@ -732,6 +732,8 @@ spec:
type: BoolString
fileName:
type: string
jsonPath:
type: string
outcomes:
items:
properties:

View File

@@ -732,6 +732,8 @@ spec:
type: BoolString
fileName:
type: string
jsonPath:
type: string
outcomes:
items:
properties:

View File

@@ -763,6 +763,8 @@ spec:
type: BoolString
fileName:
type: string
jsonPath:
type: string
outcomes:
items:
properties:

View File

@@ -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{}

View File

@@ -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 {

View File

@@ -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"`
}

View File

@@ -1092,6 +1092,9 @@
"fileName": {
"type": "string"
},
"jsonPath": {
"type": "string"
},
"outcomes": {
"type": "array",
"items": {

View File

@@ -1092,6 +1092,9 @@
"fileName": {
"type": "string"
},
"jsonPath": {
"type": "string"
},
"outcomes": {
"type": "array",
"items": {

View File

@@ -1138,6 +1138,9 @@
"fileName": {
"type": "string"
},
"jsonPath": {
"type": "string"
},
"outcomes": {
"type": "array",
"items": {