From 354a996edc6f061f3172aad201fac3a97a17fce3 Mon Sep 17 00:00:00 2001 From: Craig O'Donnell Date: Fri, 17 Jun 2022 14:43:56 -0400 Subject: [PATCH] feat: adds new yamlCompare and jsonCompare analyzers (#598) * feat: adds new yamlCompare analyzer * feat: adds new jsonCompare analyzer * outcome when for yamlCompare and jsonCompare --- pkg/analyze/analyzer.go | 30 + pkg/analyze/json_compare.go | 112 ++++ pkg/analyze/json_compare_test.go | 563 ++++++++++++++++++ pkg/analyze/yaml_compare.go | 109 ++++ pkg/analyze/yaml_compare_test.go | 452 ++++++++++++++ .../troubleshoot/v1beta2/analyzer_shared.go | 27 +- .../v1beta2/zz_generated.deepcopy.go | 71 +++ pkg/interfaceutils/interfaceutils.go | 54 ++ 8 files changed, 1415 insertions(+), 3 deletions(-) create mode 100644 pkg/analyze/json_compare.go create mode 100644 pkg/analyze/json_compare_test.go create mode 100644 pkg/analyze/yaml_compare.go create mode 100644 pkg/analyze/yaml_compare_test.go create mode 100644 pkg/interfaceutils/interfaceutils.go diff --git a/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go index a992d8706..c1fe6bf15 100644 --- a/pkg/analyze/analyzer.go +++ b/pkg/analyze/analyzer.go @@ -339,6 +339,36 @@ func Analyze(analyzer *troubleshootv1beta2.Analyze, getFile getCollectedFileCont } 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 { isExcluded, err := isExcluded(analyzer.Postgres.Exclude) if err != nil { diff --git a/pkg/analyze/json_compare.go b/pkg/analyze/json_compare.go new file mode 100644 index 000000000..4dc7a112f --- /dev/null +++ b/pkg/analyze/json_compare.go @@ -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 +} diff --git a/pkg/analyze/json_compare_test.go b/pkg/analyze/json_compare_test.go new file mode 100644 index 000000000..4fd0bd717 --- /dev/null +++ b/pkg/analyze/json_compare_test.go @@ -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) + } + }) + } +} diff --git a/pkg/analyze/yaml_compare.go b/pkg/analyze/yaml_compare.go new file mode 100644 index 000000000..9b72affb4 --- /dev/null +++ b/pkg/analyze/yaml_compare.go @@ -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 +} diff --git a/pkg/analyze/yaml_compare_test.go b/pkg/analyze/yaml_compare_test.go new file mode 100644 index 000000000..737f4a790 --- /dev/null +++ b/pkg/analyze/yaml_compare_test.go @@ -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) + } + }) + } +} diff --git a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go index 21f114fb5..9e370ba25 100644 --- a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go @@ -131,6 +131,24 @@ type TextAnalyze struct { 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 { AnalyzeMeta `json:",inline" yaml:",inline"` Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` @@ -175,9 +193,10 @@ type SysctlAnalyze struct { } type AnalyzeMeta struct { - CheckName string `json:"checkName,omitempty" yaml:"checkName,omitempty"` - Exclude *multitype.BoolOrString `json:"exclude,omitempty" yaml:"exclude,omitempty"` - Strict *multitype.BoolOrString `json:"strict,omitempty" yaml:"strict,omitempty"` + CheckName string `json:"checkName,omitempty" yaml:"checkName,omitempty"` + Exclude *multitype.BoolOrString `json:"exclude,omitempty" yaml:"exclude,omitempty"` + Strict *multitype.BoolOrString `json:"strict,omitempty" yaml:"strict,omitempty"` + Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` } type Analyze struct { @@ -197,6 +216,8 @@ type Analyze struct { Distribution *Distribution `json:"distribution,omitempty" yaml:"distribution,omitempty"` NodeResources *NodeResources `json:"nodeResources,omitempty" yaml:"nodeResources,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"` Mysql *DatabaseAnalyze `json:"mysql,omitempty" yaml:"mysql,omitempty"` Redis *DatabaseAnalyze `json:"redis,omitempty" yaml:"redis,omitempty"` diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go index 819435be5..0862fdce6 100644 --- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go @@ -134,6 +134,16 @@ func (in *Analyze) DeepCopyInto(out *Analyze) { *out = new(TextAnalyze) (*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 { in, out := &in.Postgres, &out.Postgres *out = new(DatabaseAnalyze) @@ -226,6 +236,13 @@ func (in *AnalyzeMeta) DeepCopyInto(out *AnalyzeMeta) { *out = new(multitype.BoolOrString) **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. @@ -2276,6 +2293,33 @@ func (in *JobStatus) DeepCopy() *JobStatus { 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. func (in *KernelModulesAnalyze) DeepCopyInto(out *KernelModulesAnalyze) { *out = *in @@ -4079,3 +4123,30 @@ func (in *WeaveReportAnalyze) DeepCopy() *WeaveReportAnalyze { in.DeepCopyInto(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 +} diff --git a/pkg/interfaceutils/interfaceutils.go b/pkg/interfaceutils/interfaceutils.go new file mode 100644 index 000000000..5a4fef092 --- /dev/null +++ b/pkg/interfaceutils/interfaceutils.go @@ -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 +}