feat: adds new yamlCompare and jsonCompare analyzers (#598)

* feat: adds new yamlCompare analyzer

* feat: adds new jsonCompare analyzer

* outcome when for yamlCompare and jsonCompare
This commit is contained in:
Craig O'Donnell
2022-06-17 14:43:56 -04:00
committed by GitHub
parent 1de5a79855
commit 354a996edc
8 changed files with 1415 additions and 3 deletions

View File

@@ -339,6 +339,36 @@ func Analyze(analyzer *troubleshootv1beta2.Analyze, getFile getCollectedFileCont
} }
return results, nil return results, nil
} }
if analyzer.YamlCompare != nil {
isExcluded, err := isExcluded(analyzer.YamlCompare.Exclude)
if err != nil {
return nil, err
}
if isExcluded {
return nil, nil
}
result, err := analyzeYamlCompare(analyzer.YamlCompare, getFile)
if err != nil {
return nil, err
}
result.Strict = analyzer.YamlCompare.Strict.BoolOrDefaultFalse()
return []*AnalyzeResult{result}, nil
}
if analyzer.JsonCompare != nil {
isExcluded, err := isExcluded(analyzer.JsonCompare.Exclude)
if err != nil {
return nil, err
}
if isExcluded {
return nil, nil
}
result, err := analyzeJsonCompare(analyzer.JsonCompare, getFile)
if err != nil {
return nil, err
}
result.Strict = analyzer.JsonCompare.Strict.BoolOrDefaultFalse()
return []*AnalyzeResult{result}, nil
}
if analyzer.Postgres != nil { if analyzer.Postgres != nil {
isExcluded, err := isExcluded(analyzer.Postgres.Exclude) isExcluded, err := isExcluded(analyzer.Postgres.Exclude)
if err != nil { if err != nil {

112
pkg/analyze/json_compare.go Normal file
View File

@@ -0,0 +1,112 @@
package analyzer
import (
"encoding/json"
"path/filepath"
"reflect"
"strconv"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
iutils "github.com/replicatedhq/troubleshoot/pkg/interfaceutils"
)
func analyzeJsonCompare(analyzer *troubleshootv1beta2.JsonCompare, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
fullPath := filepath.Join(analyzer.CollectorName, analyzer.FileName)
collected, err := getCollectedFileContents(fullPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
}
var actual interface{}
err = json.Unmarshal(collected, &actual)
if err != nil {
return nil, errors.Wrap(err, "failed to parse collected data as json")
}
if analyzer.Path != "" {
actual, err = iutils.GetAtPath(actual, analyzer.Path)
if err != nil {
return nil, errors.Wrapf(err, "failed to get object at path: %s", analyzer.Path)
}
}
var expected interface{}
err = json.Unmarshal([]byte(analyzer.Value), &expected)
if err != nil {
return nil, errors.Wrap(err, "failed to parse expected value as json")
}
title := analyzer.CheckName
if title == "" {
title = analyzer.CollectorName
}
result := &AnalyzeResult{
Title: title,
IconKey: "kubernetes_text_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
}
equal := reflect.DeepEqual(actual, expected)
for _, outcome := range analyzer.Outcomes {
if outcome.Fail != nil {
when := false
if outcome.Fail.When != "" {
when, err = strconv.ParseBool(outcome.Fail.When)
if err != nil {
return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Fail.When)
}
}
if when == equal {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI
return result, nil
}
} else if outcome.Warn != nil {
when := false
if outcome.Warn.When != "" {
when, err = strconv.ParseBool(outcome.Warn.When)
if err != nil {
return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Warn.When)
}
}
if when == equal {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
return result, nil
}
} else if outcome.Pass != nil {
when := true // default to passing when values are equal
if outcome.Pass.When != "" {
when, err = strconv.ParseBool(outcome.Pass.When)
if err != nil {
return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Pass.When)
}
}
if when == equal {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
return result, nil
}
}
}
return &AnalyzeResult{
Title: title,
IconKey: "kubernetes_text_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
IsFail: true,
Message: "Invalid analyzer",
}, nil
}

View File

@@ -0,0 +1,563 @@
package analyzer
import (
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/stretchr/testify/require"
)
func Test_jsonCompare(t *testing.T) {
tests := []struct {
name string
isError bool
analyzer troubleshootv1beta2.JsonCompare
expectResult AnalyzeResult
fileContents []byte
}{
{
name: "basic comparison",
analyzer: troubleshootv1beta2.JsonCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "json-compare-1",
FileName: "json-compare-1.json",
Value: `{
"foo": "bar",
"stuff": {
"foo": "bar",
"bar": true
},
"morestuff": [
{
"foo": {
"bar": 123
}
}
]
}`,
},
expectResult: AnalyzeResult{
IsPass: true,
IsWarn: false,
IsFail: false,
Title: "json-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: "basic 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: "json-compare-1-1",
FileName: "json-compare-1-1.json",
Value: `{
"foo": "bar",
"stuff": {
"foo": "bar",
"bar": true
},
"morestuff": [
{
"foo": {
"bar": 123
}
}
]
}`,
},
expectResult: AnalyzeResult{
IsPass: false,
IsWarn: false,
IsFail: true,
Title: "json-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: "comparison using path 1",
analyzer: troubleshootv1beta2.JsonCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "json-compare-2",
FileName: "json-compare-2.json",
Path: "morestuff",
Value: `[
{
"foo": {
"bar": 123
}
}
]`,
},
expectResult: AnalyzeResult{
IsPass: true,
IsWarn: false,
IsFail: false,
Title: "json-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
}
}
]
}`),
},
{
name: "comparison using path 2",
analyzer: troubleshootv1beta2.JsonCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "json-compare-3",
FileName: "json-compare-3.json",
Path: "morestuff.[0].foo.bar",
Value: `123`,
},
expectResult: AnalyzeResult{
IsPass: true,
IsWarn: false,
IsFail: false,
Title: "json-compare-3",
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: "comparison using path 2, but warn on match",
analyzer: troubleshootv1beta2.JsonCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
When: "false",
},
},
{
Warn: &troubleshootv1beta2.SingleOutcome{
Message: "warn",
When: "true",
},
},
},
CollectorName: "json-compare-3-1",
FileName: "json-compare-3-1.json",
Path: "morestuff.[0].foo.bar",
Value: `123`,
},
expectResult: AnalyzeResult{
IsPass: false,
IsWarn: true,
IsFail: false,
Title: "json-compare-3-1",
Message: "warn",
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: "basic comparison fail",
analyzer: troubleshootv1beta2.JsonCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "json-compare-4",
FileName: "json-compare-4.json",
Value: `{
"foo": "bar",
"stuff": {
"foo": "bar",
"bar": true
},
"morestuff": [
{
"foo": {
"bar": 123
}
}
]
}`,
},
expectResult: AnalyzeResult{
IsPass: false,
IsWarn: false,
IsFail: true,
Title: "json-compare-4",
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
},
"otherstuff": [
{
"foo": {
"bar": 123
}
}
]
}`),
},
{
name: "comparison using path fail 1",
analyzer: troubleshootv1beta2.JsonCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "json-compare-5",
FileName: "json-compare-5.json",
Path: "morestuff",
Value: `[
{
"foo": {
"bar": 321
}
}
]`,
},
expectResult: AnalyzeResult{
IsPass: false,
IsWarn: false,
IsFail: true,
Title: "json-compare-5",
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: "comparison using path, but pass when not matching",
analyzer: troubleshootv1beta2.JsonCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
When: "false",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
When: "true",
},
},
},
CollectorName: "json-compare-5-1",
FileName: "json-compare-5-1.json",
Path: "morestuff",
Value: `[
{
"foo": {
"bar": 321
}
}
]`,
},
expectResult: AnalyzeResult{
IsPass: true,
IsWarn: false,
IsFail: false,
Title: "json-compare-5-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: "basic comparison warn",
analyzer: troubleshootv1beta2.JsonCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Warn: &troubleshootv1beta2.SingleOutcome{
Message: "warn",
},
},
},
CollectorName: "json-compare-6",
FileName: "json-compare-6.json",
Value: `{
"foo": "bar",
"stuff": {
"foo": "bar",
"bar": true
},
"morestuff": [
{
"foo": {
"bar": 123
}
}
]
}`,
},
expectResult: AnalyzeResult{
IsPass: false,
IsWarn: true,
IsFail: false,
Title: "json-compare-6",
Message: "warn",
IconKey: "kubernetes_text_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
},
fileContents: []byte(`{
"foo": "bar",
"stuff": {
"foo": "bar",
"bar": true
},
"otherstuff": [
{
"foo": {
"bar": 123
}
}
]
}`),
},
{
name: "invalid json error",
isError: true,
analyzer: troubleshootv1beta2.JsonCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "json-compare-7",
FileName: "json-compare-7.json",
Path: "morestuff",
Value: `[
{
"foo": {
"bar": 123
}
}
]`,
},
fileContents: []byte(`{ "this: - is-invalid: json }`),
},
{
name: "no json error",
isError: true,
analyzer: troubleshootv1beta2.JsonCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "json-compare-8",
FileName: "json-compare-8.json",
Path: "morestuff",
Value: `[
{
"foo": {
"bar": 123
}
}
]`,
},
fileContents: []byte(``),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
getCollectedFileContents := func(n string) ([]byte, error) {
return test.fileContents, nil
}
actual, err := analyzeJsonCompare(&test.analyzer, getCollectedFileContents)
if !test.isError {
req.NoError(err)
req.Equal(test.expectResult, *actual)
} else {
req.Error(err)
}
})
}
}

109
pkg/analyze/yaml_compare.go Normal file
View File

@@ -0,0 +1,109 @@
package analyzer
import (
"path/filepath"
"reflect"
"strconv"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
iutils "github.com/replicatedhq/troubleshoot/pkg/interfaceutils"
"gopkg.in/yaml.v2"
)
func analyzeYamlCompare(analyzer *troubleshootv1beta2.YamlCompare, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
fullPath := filepath.Join(analyzer.CollectorName, analyzer.FileName)
collected, err := getCollectedFileContents(fullPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
}
var actual interface{}
err = yaml.Unmarshal(collected, &actual)
if err != nil {
return nil, errors.Wrap(err, "failed to parse collected data as yaml doc")
}
if analyzer.Path != "" {
actual, err = iutils.GetAtPath(actual, analyzer.Path)
if err != nil {
return nil, errors.Wrapf(err, "failed to get object at path: %s", analyzer.Path)
}
}
var expected interface{}
err = yaml.Unmarshal([]byte(analyzer.Value), &expected)
if err != nil {
return nil, errors.Wrap(err, "failed to parse expected value as yaml doc")
}
title := analyzer.CheckName
if title == "" {
title = analyzer.CollectorName
}
result := &AnalyzeResult{
Title: title,
IconKey: "kubernetes_text_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
}
equal := reflect.DeepEqual(actual, expected)
for _, outcome := range analyzer.Outcomes {
if outcome.Fail != nil {
when := false
if outcome.Fail.When != "" {
when, err = strconv.ParseBool(outcome.Fail.When)
if err != nil {
return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Fail.When)
}
}
if when == equal {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI
return result, nil
}
} else if outcome.Warn != nil {
when := false
if outcome.Warn.When != "" {
when, err = strconv.ParseBool(outcome.Warn.When)
if err != nil {
return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Warn.When)
}
}
if when == equal {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
return result, nil
}
} else if outcome.Pass != nil {
when := true // default to passing when values are equal
if outcome.Pass.When != "" {
when, err = strconv.ParseBool(outcome.Pass.When)
if err != nil {
return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Pass.When)
}
}
if when == equal {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
return result, nil
}
}
}
return &AnalyzeResult{
Title: title,
IconKey: "kubernetes_text_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
IsFail: true,
Message: "Invalid analyzer",
}, nil
}

View File

@@ -0,0 +1,452 @@
package analyzer
import (
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/stretchr/testify/require"
)
func Test_yamlCompare(t *testing.T) {
tests := []struct {
name string
isError bool
analyzer troubleshootv1beta2.YamlCompare
expectResult AnalyzeResult
fileContents []byte
}{
{
name: "basic comparison",
analyzer: troubleshootv1beta2.YamlCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "yaml-compare-1",
FileName: "yaml-compare-1.yaml",
Value: `foo: bar
stuff:
foo: bar
bar: foo
morestuff:
- foo:
bar: baz`,
},
expectResult: AnalyzeResult{
IsPass: true,
IsWarn: false,
IsFail: false,
Title: "yaml-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: foo
morestuff:
- foo:
bar: baz`),
},
{
name: "basic comparison, but fail on match",
analyzer: troubleshootv1beta2.YamlCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
When: "false",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
When: "true",
},
},
},
CollectorName: "yaml-compare-1-1",
FileName: "yaml-compare-1-1.yaml",
Value: `foo: bar
stuff:
foo: bar
bar: foo
morestuff:
- foo:
bar: baz`,
},
expectResult: AnalyzeResult{
IsPass: false,
IsWarn: false,
IsFail: true,
Title: "yaml-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: foo
morestuff:
- foo:
bar: baz`),
},
{
name: "comparison using path 1",
analyzer: troubleshootv1beta2.YamlCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "yaml-compare-2",
FileName: "yaml-compare-2.yaml",
Path: "morestuff",
Value: `- foo:
bar: baz`,
},
expectResult: AnalyzeResult{
IsPass: true,
IsWarn: false,
IsFail: false,
Title: "yaml-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: foo
morestuff:
- foo:
bar: baz`),
},
{
name: "comparison using path, but warn when matching",
analyzer: troubleshootv1beta2.YamlCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
When: "false",
},
},
{
Warn: &troubleshootv1beta2.SingleOutcome{
Message: "warn",
When: "true",
},
},
},
CollectorName: "yaml-compare-2-1",
FileName: "yaml-compare-2-1.yaml",
Path: "morestuff",
Value: `- foo:
bar: baz`,
},
expectResult: AnalyzeResult{
IsPass: false,
IsWarn: true,
IsFail: false,
Title: "yaml-compare-2-1",
Message: "warn",
IconKey: "kubernetes_text_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
},
fileContents: []byte(`foo: bar
stuff:
foo: bar
bar: foo
morestuff:
- foo:
bar: baz`),
},
{
name: "comparison using path 2",
analyzer: troubleshootv1beta2.YamlCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "yaml-compare-3",
FileName: "yaml-compare-3.yaml",
Path: "morestuff.[0].foo",
Value: `bar: baz`,
},
expectResult: AnalyzeResult{
IsPass: true,
IsWarn: false,
IsFail: false,
Title: "yaml-compare-3",
Message: "pass",
IconKey: "kubernetes_text_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
},
fileContents: []byte(`foo: bar
stuff:
foo: bar
bar: foo
morestuff:
- foo:
bar: baz`),
},
{
name: "basic comparison fail",
analyzer: troubleshootv1beta2.YamlCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "yaml-compare-4",
FileName: "yaml-compare-4.yaml",
Value: `foo: bar
stuff:
foo: bar
bar: foo
morestuff:
- foo:
bar: baz`,
},
expectResult: AnalyzeResult{
IsPass: false,
IsWarn: false,
IsFail: true,
Title: "yaml-compare-4",
Message: "fail",
IconKey: "kubernetes_text_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
},
fileContents: []byte(`foo: bar
stuff:
foo: bar
bar: foo
otherstuff:
- foo:
bar: baz`),
},
{
name: "basic comparison pass when not matching",
analyzer: troubleshootv1beta2.YamlCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
When: "false",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
When: "true",
},
},
},
CollectorName: "yaml-compare-4-1",
FileName: "yaml-compare-4-1.yaml",
Value: `foo: bar
stuff:
foo: bar
bar: foo
morestuff:
- foo:
bar: baz`,
},
expectResult: AnalyzeResult{
IsPass: true,
IsWarn: false,
IsFail: false,
Title: "yaml-compare-4-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: foo
otherstuff:
- foo:
bar: baz`),
},
{
name: "comparison using path fail 1",
analyzer: troubleshootv1beta2.YamlCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "yaml-compare-5",
FileName: "yaml-compare-5.yaml",
Path: "morestuff",
Value: `- bar:
foo: baz`,
},
expectResult: AnalyzeResult{
IsPass: false,
IsWarn: false,
IsFail: true,
Title: "yaml-compare-5",
Message: "fail",
IconKey: "kubernetes_text_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
},
fileContents: []byte(`foo: bar
stuff:
foo: bar
bar: foo
morestuff:
- foo:
bar: baz`),
},
{
name: "basic comparison warn",
analyzer: troubleshootv1beta2.YamlCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Warn: &troubleshootv1beta2.SingleOutcome{
Message: "warn",
},
},
},
CollectorName: "yaml-compare-6",
FileName: "yaml-compare-6.yaml",
Value: `foo: bar
stuff:
foo: bar
bar: foo
morestuff:
- foo:
bar: baz`,
},
expectResult: AnalyzeResult{
IsPass: false,
IsWarn: true,
IsFail: false,
Title: "yaml-compare-6",
Message: "warn",
IconKey: "kubernetes_text_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
},
fileContents: []byte(`foo: bar
stuff:
foo: bar
bar: foo
otherstuff:
- foo:
bar: baz`),
},
{
name: "invalid yaml error",
isError: true,
analyzer: troubleshootv1beta2.YamlCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "yaml-compare-7",
FileName: "yaml-compare-7.yaml",
Path: "morestuff",
Value: `- foo:
bar: baz`,
},
fileContents: []byte(`{ "this: - is-invalid: yaml }`),
},
{
name: "no yaml error",
isError: true,
analyzer: troubleshootv1beta2.YamlCompare{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "yaml-compare-8",
FileName: "yaml-compare-8.yaml",
Path: "morestuff",
Value: `- foo:
bar: baz`,
},
fileContents: []byte(``),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
getCollectedFileContents := func(n string) ([]byte, error) {
return test.fileContents, nil
}
actual, err := analyzeYamlCompare(&test.analyzer, getCollectedFileContents)
if !test.isError {
req.NoError(err)
req.Equal(test.expectResult, *actual)
} else {
req.Error(err)
}
})
}
}

View File

@@ -131,6 +131,24 @@ type TextAnalyze struct {
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
} }
type YamlCompare struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
FileName string `json:"fileName,omitempty" yaml:"fileName,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
Value string `json:"value,omitempty" yaml:"value,omitempty"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type JsonCompare struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
FileName string `json:"fileName,omitempty" yaml:"fileName,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
Value string `json:"value,omitempty" yaml:"value,omitempty"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type DatabaseAnalyze struct { type DatabaseAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"` AnalyzeMeta `json:",inline" yaml:",inline"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
@@ -175,9 +193,10 @@ type SysctlAnalyze struct {
} }
type AnalyzeMeta struct { type AnalyzeMeta struct {
CheckName string `json:"checkName,omitempty" yaml:"checkName,omitempty"` CheckName string `json:"checkName,omitempty" yaml:"checkName,omitempty"`
Exclude *multitype.BoolOrString `json:"exclude,omitempty" yaml:"exclude,omitempty"` Exclude *multitype.BoolOrString `json:"exclude,omitempty" yaml:"exclude,omitempty"`
Strict *multitype.BoolOrString `json:"strict,omitempty" yaml:"strict,omitempty"` Strict *multitype.BoolOrString `json:"strict,omitempty" yaml:"strict,omitempty"`
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
} }
type Analyze struct { type Analyze struct {
@@ -197,6 +216,8 @@ type Analyze struct {
Distribution *Distribution `json:"distribution,omitempty" yaml:"distribution,omitempty"` Distribution *Distribution `json:"distribution,omitempty" yaml:"distribution,omitempty"`
NodeResources *NodeResources `json:"nodeResources,omitempty" yaml:"nodeResources,omitempty"` NodeResources *NodeResources `json:"nodeResources,omitempty" yaml:"nodeResources,omitempty"`
TextAnalyze *TextAnalyze `json:"textAnalyze,omitempty" yaml:"textAnalyze,omitempty"` TextAnalyze *TextAnalyze `json:"textAnalyze,omitempty" yaml:"textAnalyze,omitempty"`
YamlCompare *YamlCompare `json:"yamlCompare,omitempty" yaml:"yamlCompare,omitempty"`
JsonCompare *JsonCompare `json:"jsonCompare,omitempty" yaml:"jsonCompare,omitempty"`
Postgres *DatabaseAnalyze `json:"postgres,omitempty" yaml:"postgres,omitempty"` Postgres *DatabaseAnalyze `json:"postgres,omitempty" yaml:"postgres,omitempty"`
Mysql *DatabaseAnalyze `json:"mysql,omitempty" yaml:"mysql,omitempty"` Mysql *DatabaseAnalyze `json:"mysql,omitempty" yaml:"mysql,omitempty"`
Redis *DatabaseAnalyze `json:"redis,omitempty" yaml:"redis,omitempty"` Redis *DatabaseAnalyze `json:"redis,omitempty" yaml:"redis,omitempty"`

View File

@@ -134,6 +134,16 @@ func (in *Analyze) DeepCopyInto(out *Analyze) {
*out = new(TextAnalyze) *out = new(TextAnalyze)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.YamlCompare != nil {
in, out := &in.YamlCompare, &out.YamlCompare
*out = new(YamlCompare)
(*in).DeepCopyInto(*out)
}
if in.JsonCompare != nil {
in, out := &in.JsonCompare, &out.JsonCompare
*out = new(JsonCompare)
(*in).DeepCopyInto(*out)
}
if in.Postgres != nil { if in.Postgres != nil {
in, out := &in.Postgres, &out.Postgres in, out := &in.Postgres, &out.Postgres
*out = new(DatabaseAnalyze) *out = new(DatabaseAnalyze)
@@ -226,6 +236,13 @@ func (in *AnalyzeMeta) DeepCopyInto(out *AnalyzeMeta) {
*out = new(multitype.BoolOrString) *out = new(multitype.BoolOrString)
**out = **in **out = **in
} }
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalyzeMeta. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalyzeMeta.
@@ -2276,6 +2293,33 @@ func (in *JobStatus) DeepCopy() *JobStatus {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JsonCompare) DeepCopyInto(out *JsonCompare) {
*out = *in
in.AnalyzeMeta.DeepCopyInto(&out.AnalyzeMeta)
if in.Outcomes != nil {
in, out := &in.Outcomes, &out.Outcomes
*out = make([]*Outcome, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(Outcome)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JsonCompare.
func (in *JsonCompare) DeepCopy() *JsonCompare {
if in == nil {
return nil
}
out := new(JsonCompare)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KernelModulesAnalyze) DeepCopyInto(out *KernelModulesAnalyze) { func (in *KernelModulesAnalyze) DeepCopyInto(out *KernelModulesAnalyze) {
*out = *in *out = *in
@@ -4079,3 +4123,30 @@ func (in *WeaveReportAnalyze) DeepCopy() *WeaveReportAnalyze {
in.DeepCopyInto(out) in.DeepCopyInto(out)
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *YamlCompare) DeepCopyInto(out *YamlCompare) {
*out = *in
in.AnalyzeMeta.DeepCopyInto(&out.AnalyzeMeta)
if in.Outcomes != nil {
in, out := &in.Outcomes, &out.Outcomes
*out = make([]*Outcome, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(Outcome)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YamlCompare.
func (in *YamlCompare) DeepCopy() *YamlCompare {
if in == nil {
return nil
}
out := new(YamlCompare)
in.DeepCopyInto(out)
return out
}

View File

@@ -0,0 +1,54 @@
package interfaceutils
import (
"fmt"
"strconv"
"strings"
"github.com/pkg/errors"
)
func GetAtPath(input interface{}, path string) (interface{}, error) {
parts := strings.SplitN(path, ".", 2)
key := parts[0]
if isArrayIndex(key) {
i, err := getArrayIndexValue(key)
if err != nil {
return nil, errors.Wrapf(err, "failed to get index value of %s", key)
}
obj, ok := input.([]interface{})
if !ok {
return nil, errors.New(fmt.Sprintf("input is not an array: %+v", input))
}
input = obj[i]
} else {
switch t := input.(type) {
case map[interface{}]interface{}:
input = input.(map[interface{}]interface{})[key]
case map[string]interface{}:
input = input.(map[string]interface{})[key]
default:
return nil, errors.New(fmt.Sprintf("input is not a map, but rather a %v: %+v", t, input))
}
}
if len(parts) > 1 {
return GetAtPath(input, parts[1])
}
return input, nil
}
func isArrayIndex(key string) bool {
return strings.HasPrefix(key, "[") && strings.HasSuffix(key, "]")
}
func getArrayIndexValue(key string) (int, error) {
key = strings.TrimPrefix(key, "[")
key = strings.TrimSuffix(key, "]")
i, err := strconv.Atoi(key)
if err != nil {
return -1, errors.Wrapf(err, "failed to parse index %s", key)
}
return i, nil
}