diff --git a/Makefile b/Makefile index dc40d45f2..9767d86d1 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ test-all: lint test .PHONY: test test: - go test -race ./... + go test -race -count=1 ./... .PHONY: lint lint: diff --git a/file/readfile.go b/file/readfile.go index 93054c8e3..d72b0742e 100644 --- a/file/readfile.go +++ b/file/readfile.go @@ -20,44 +20,48 @@ import ( // file, depending on the type of each item in filenames, merges the content of // these files and renders a Content. func getContent(filenames []string) (*Content, error) { - var allReaders []io.Reader var workspaces []string + var res Content + var errs []error for _, fileOrDir := range filenames { readers, err := getReaders(fileOrDir) if err != nil { return nil, err } - allReaders = append(allReaders, readers...) - } - var res Content - for _, r := range allReaders { - content, err := readContent(r) - if err != nil { - return nil, fmt.Errorf("reading file: %w", err) - } - if content.Workspace != "" { - workspaces = append(workspaces, content.Workspace) - } - err = mergo.Merge(&res, content, mergo.WithAppendSlice) - if err != nil { - return nil, fmt.Errorf("merging file contents: %w", err) + + for filename, r := range readers { + content, err := readContent(r) + if err != nil { + errs = append(errs, fmt.Errorf("reading file %s: %w", filename, err)) + continue + } + if content.Workspace != "" { + workspaces = append(workspaces, content.Workspace) + } + err = mergo.Merge(&res, content, mergo.WithAppendSlice) + if err != nil { + return nil, fmt.Errorf("merging file contents: %w", err) + } } } + if len(errs) > 0 { + return nil, utils.ErrArray{Errors: errs} + } if err := validateWorkspaces(workspaces); err != nil { return nil, err } return &res, nil } -// getReaders returns back io.Readers representing all the YAML and JSON -// files in a directory. If fileOrDir is a single file, then it +// getReaders returns back a map of filename:io.Reader representing all the +// YAML and JSON files in a directory. If fileOrDir is a single file, then it // returns back the reader for the file. // If fileOrDir is equal to "-" string, then it returns back a io.Reader // for the os.Stdin file descriptor. -func getReaders(fileOrDir string) ([]io.Reader, error) { +func getReaders(fileOrDir string) (map[string]io.Reader, error) { // special case where `-` means stdin if fileOrDir == "-" { - return []io.Reader{os.Stdin}, nil + return map[string]io.Reader{"STDIN": os.Stdin}, nil } finfo, err := os.Stat(fileOrDir) @@ -75,13 +79,13 @@ func getReaders(fileOrDir string) ([]io.Reader, error) { files = append(files, fileOrDir) } - var res []io.Reader + res := make(map[string]io.Reader, len(files)) for _, file := range files { f, err := os.Open(file) if err != nil { return nil, fmt.Errorf("opening file: %w", err) } - res = append(res, bufio.NewReader(f)) + res[file] = bufio.NewReader(f) } return res, nil } diff --git a/file/readfile_test.go b/file/readfile_test.go index df2a49285..eb5e65e10 100644 --- a/file/readfile_test.go +++ b/file/readfile_test.go @@ -6,9 +6,10 @@ import ( "reflect" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/kong/deck/utils" "github.com/kong/go-kong/kong" - "github.com/stretchr/testify/require" ) func Test_configFilesInDir(t *testing.T) { @@ -66,15 +67,17 @@ func Test_getReaders(t *testing.T) { tests := []struct { name string args args - want []io.Reader + want map[string]io.Reader // length of returned array wantLen int wantErr bool }{ { - name: "read from standard input", - args: args{"-"}, - want: []io.Reader{os.Stdin}, + name: "read from standard input", + args: args{"-"}, + want: map[string]io.Reader{ + "STDIN": os.Stdin, + }, wantLen: 1, wantErr: false, }, @@ -126,6 +129,29 @@ func Test_getReaders(t *testing.T) { } } +func sortSlices(x, y interface{}) bool { + var xName, yName string + switch xEntity := x.(type) { + case FService: + yEntity := y.(FService) + xName = *xEntity.Name + yName = *yEntity.Name + case FRoute: + yEntity := y.(FRoute) + xName = *xEntity.Name + yName = *yEntity.Name + case FConsumer: + yEntity := y.(FConsumer) + xName = *xEntity.Username + yName = *yEntity.Username + case FPlugin: + yEntity := y.(FPlugin) + xName = *xEntity.Name + yName = *yEntity.Name + } + return xName < yName +} + func Test_getContent(t *testing.T) { type args struct { filenames []string @@ -556,7 +582,15 @@ kong.log.set_serialize_value("span_id", parse_traceid(ngx.ctx.KONG_SPANS[1].span t.Errorf("getContent() error = %v, wantErr %v", err, tt.wantErr) return } - require.Equal(t, tt.want, got) + + opt := []cmp.Option{ + cmpopts.SortSlices(sortSlices), + cmpopts.SortSlices(func(a, b *string) bool { return *a < *b }), + cmpopts.EquateEmpty(), + } + if diff := cmp.Diff(got, tt.want, opt...); diff != "" { + t.Errorf(diff) + } }) } } diff --git a/file/validate.go b/file/validate.go index 14076950c..1f3eb977a 100644 --- a/file/validate.go +++ b/file/validate.go @@ -1,6 +1,8 @@ package file import ( + "encoding/json" + "errors" "fmt" "github.com/kong/deck/utils" @@ -8,6 +10,15 @@ import ( "sigs.k8s.io/yaml" ) +type ValidationError struct { + Object string `json:"object"` + Err error `json:"error"` +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf("validation error: object=%s, err=%v", e.Object, e.Err) +} + func validate(content []byte) error { var c map[string]interface{} err := yaml.Unmarshal(content, &c) @@ -24,10 +35,14 @@ func validate(content []byte) error { if result.Valid() { return nil } + var errs utils.ErrArray for _, desc := range result.Errors() { - err := fmt.Errorf(desc.String()) - errs.Errors = append(errs.Errors, err) + jsonString, err := json.Marshal(desc.Value()) + if err != nil { + return err + } + errs.Errors = append(errs.Errors, &ValidationError{Object: string(jsonString), Err: errors.New(desc.String())}) } return errs }