From 01f2f19d9f48509fbbd6c76d817f61a9a0339dee Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Fri, 2 Aug 2019 10:51:14 -0700 Subject: [PATCH 1/2] add json output formatter --- go.mod | 2 +- kubeval/output.go | 101 +++++++++++++++++++++++++++++++-- kubeval/output_test.go | 123 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 kubeval/output_test.go diff --git a/go.mod b/go.mod index a4f4821..a5ec77f 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834 // indirect github.com/spf13/pflag v0.0.0-20180821114517-d929dcbb1086 // indirect github.com/spf13/viper v1.1.0 - github.com/stretchr/testify v1.3.0 // indirect + github.com/stretchr/testify v1.3.0 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609 diff --git a/kubeval/output.go b/kubeval/output.go index d4fec70..ce84d59 100644 --- a/kubeval/output.go +++ b/kubeval/output.go @@ -1,7 +1,12 @@ package kubeval import ( - "github.com/instrumenta/kubeval/log" + "bytes" + "encoding/json" + "log" + "os" + + kLog "github.com/instrumenta/kubeval/log" ) // TODO (brendanryan) move these structs to `/log` once we have removed the potential @@ -25,16 +30,16 @@ func NewSTDOutputManager() *STDOutputManager { func (s *STDOutputManager) Put(result ValidationResult) error { if len(result.Errors) > 0 { - log.Warn("The file", result.FileName, "contains an invalid", result.Kind) + kLog.Warn("The file", result.FileName, "contains an invalid", result.Kind) for _, desc := range result.Errors { - log.Info("--->", desc) + kLog.Info("--->", desc) } } else if result.Kind == "" { - log.Success("The file", result.FileName, "contains an empty YAML document") + kLog.Success("The file", result.FileName, "contains an empty YAML document") } else if !result.ValidatedAgainstSchema { - log.Warn("The file", result.FileName, "containing a", result.Kind, "was not validated against a schema") + kLog.Warn("The file", result.FileName, "containing a", result.Kind, "was not validated against a schema") } else { - log.Success("The file", result.FileName, "contains a valid", result.Kind) + kLog.Success("The file", result.FileName, "contains a valid", result.Kind) } return nil @@ -44,3 +49,87 @@ func (s *STDOutputManager) Flush() error { // no op return nil } + +type status string + +const ( + statusInvalid = "invalid" + statusValid = "valid" + statusSkipped = "skipped" +) + +type jsonEvalResult struct { + Filename string `json:"filename"` + Kind string `json:"kind"` + Status status `json:"status"` + Errors []string `json:"errors"` +} + +// jsonOutputManager reports `ccheck` results to `stdout` as a json array.. +type jsonOutputManager struct { + logger *log.Logger + + data []jsonEvalResult +} + +func newDefaultJSONOutputManager() *jsonOutputManager { + return newJSONOutputManager(log.New(os.Stdout, "", 0)) +} + +func newJSONOutputManager(l *log.Logger) *jsonOutputManager { + return &jsonOutputManager{ + logger: l, + } +} + +func getStatus(r ValidationResult) status { + if r.Kind == "" { + return statusSkipped + } + + if !r.ValidatedAgainstSchema { + return statusSkipped + } + + if len(r.Errors) > 0 { + return statusInvalid + } + + return statusValid +} + +func (j *jsonOutputManager) put(r ValidationResult) error { + + // stringify gojsonschema errors + // use a pre-allocated slice to ensure the json will have an + // empty array in the "zero" case + errs := make([]string, 0, len(r.Errors)) + for _, e := range r.Errors { + errs = append(errs, e.String()) + } + + j.data = append(j.data, jsonEvalResult{ + Filename: r.FileName, + Kind: r.Kind, + Status: getStatus(r), + Errors: errs, + }) + + return nil +} + +func (j *jsonOutputManager) flush() error { + b, err := json.Marshal(j.data) + if err != nil { + return err + } + + var out bytes.Buffer + err = json.Indent(&out, b, "", "\t") + if err != nil { + return err + } + + j.logger.Print(out.String()) + return nil +} diff --git a/kubeval/output_test.go b/kubeval/output_test.go new file mode 100644 index 0000000..c5746e1 --- /dev/null +++ b/kubeval/output_test.go @@ -0,0 +1,123 @@ +package kubeval + +import ( + "bytes" + "log" + "testing" + + "github.com/xeipuuv/gojsonschema" + + "github.com/stretchr/testify/assert" +) + +func newResultError(msg string) gojsonschema.ResultError { + r := &gojsonschema.ResultErrorFields{} + + r.SetContext(gojsonschema.NewJsonContext("error", nil)) + r.SetDescription(msg) + + return r +} + +func newResultErrors(msgs []string) []gojsonschema.ResultError { + var res []gojsonschema.ResultError + for _, m := range msgs { + res = append(res, newResultError(m)) + } + return res +} + +func Test_jsonOutputManager_put(t *testing.T) { + type args struct { + vr ValidationResult + } + + tests := []struct { + msg string + args args + exp string + expErr error + }{ + { + msg: "empty input", + args: args{ + vr: ValidationResult{}, + }, + exp: `[ + { + "filename": "", + "kind": "", + "status": "skipped", + "errors": [] + } +] +`, + }, + { + msg: "file with no errors", + args: args{ + vr: ValidationResult{ + FileName: "deployment.yaml", + Kind: "deployment", + ValidatedAgainstSchema: true, + Errors: nil, + }, + }, + exp: `[ + { + "filename": "deployment.yaml", + "kind": "deployment", + "status": "valid", + "errors": [] + } +] +`, + }, + { + msg: "file with errors", + args: args{ + vr: ValidationResult{ + FileName: "service.yaml", + Kind: "service", + ValidatedAgainstSchema: true, + Errors: newResultErrors([]string{ + "i am a error", + "i am another error", + }), + }, + }, + exp: `[ + { + "filename": "service.yaml", + "kind": "service", + "status": "invalid", + "errors": [ + "error: i am a error", + "error: i am another error" + ] + } +] +`, + }, + } + for _, tt := range tests { + t.Run(tt.msg, func(t *testing.T) { + buf := new(bytes.Buffer) + s := newJSONOutputManager(log.New(buf, "", 0)) + + // record results + err := s.put(tt.args.vr) + if err != nil { + assert.Equal(t, tt.expErr, err) + } + + // flush final buffer + err = s.flush() + if err != nil { + assert.Equal(t, tt.expErr, err) + } + + assert.Equal(t, tt.exp, buf.String()) + }) + } +} From 063915fe7be3b415442a1c9883b7c56b90716d76 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Fri, 2 Aug 2019 12:42:12 -0700 Subject: [PATCH 2/2] cleanup --- kubeval/output.go | 1 - 1 file changed, 1 deletion(-) diff --git a/kubeval/output.go b/kubeval/output.go index ce84d59..7d2d194 100644 --- a/kubeval/output.go +++ b/kubeval/output.go @@ -99,7 +99,6 @@ func getStatus(r ValidationResult) status { } func (j *jsonOutputManager) put(r ValidationResult) error { - // stringify gojsonschema errors // use a pre-allocated slice to ensure the json will have an // empty array in the "zero" case