From 740cfc68af4eee955f5e4dd452e6a472be542de7 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Thu, 28 Mar 2024 08:54:07 +0100 Subject: [PATCH 1/7] wip for validate diag rendering --- bundle/tests/basic/databricks.yml | 11 +++++++ cmd/bundle/validate.go | 45 ++++++++++++++++++++------ libs/diag/diagnostic.go | 2 ++ libs/dyn/convert/normalize.go | 53 ++++++++++++++++--------------- libs/dyn/path.go | 4 +++ 5 files changed, 79 insertions(+), 36 deletions(-) diff --git a/bundle/tests/basic/databricks.yml b/bundle/tests/basic/databricks.yml index de02d20bc6..74bcfd35a3 100644 --- a/bundle/tests/basic/databricks.yml +++ b/bundle/tests/basic/databricks.yml @@ -1,2 +1,13 @@ bundle: name: basic + +foobar: unknown + +resources: + jobs: + my_job: + tasks: + - task_key: task_1 + - task_key: task_2 + - task_key: task_3 + task_property: unknown diff --git a/cmd/bundle/validate.go b/cmd/bundle/validate.go index e625539b41..c42dc302cd 100644 --- a/cmd/bundle/validate.go +++ b/cmd/bundle/validate.go @@ -1,16 +1,30 @@ package bundle import ( - "encoding/json" + "path/filepath" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/diag" "github.com/spf13/cobra" ) +// "red": color.RedString, +// "green": color.GreenString, +// "blue": color.BlueString, +// "yellow": color.YellowString, +// "magenta": color.MagentaString, +// "cyan": color.CyanString, + +const tmpl = ` + {{ "Warning" | yellow }}: {{ .Summary }} + {{ "At " }}{{ .Path.String | green }} + {{ "In " }}{{ .Location.String | cyan }} +` + func newValidateCommand() *cobra.Command { cmd := &cobra.Command{ Use: "validate", @@ -25,22 +39,33 @@ func newValidateCommand() *cobra.Command { return diags.Error() } - diags = bundle.Apply(ctx, b, phases.Initialize()) + diags = diags.Extend(bundle.Apply(ctx, b, phases.Initialize())) if err := diags.Error(); err != nil { return err } // Until we change up the output of this command to be a text representation, // we'll just output all diagnostics as debug logs. - for _, diag := range diags { - log.Debugf(cmd.Context(), "[%s]: %s", diag.Location, diag.Summary) - } + for _, d := range diags { + switch d.Severity { + case diag.Warning: + // Make file relative to bundle root + out, _ := filepath.Rel(b.RootPath, d.Location.File) + d.Location.File = out + err := cmdio.RenderWithTemplate(ctx, d, "", tmpl) + if err != nil { + return err + } + } - buf, err := json.MarshalIndent(b.Config, "", " ") - if err != nil { - return err + // log.Debugf(cmd.Context(), "[%s]: %s", diag.Location, diag.Summary) } - cmd.OutOrStdout().Write(buf) + + // buf, err := json.MarshalIndent(b.Config, "", " ") + // if err != nil { + // return err + // } + // cmd.OutOrStdout().Write(buf) return nil } diff --git a/libs/diag/diagnostic.go b/libs/diag/diagnostic.go index 68b4ad611b..f65f626610 100644 --- a/libs/diag/diagnostic.go +++ b/libs/diag/diagnostic.go @@ -20,6 +20,8 @@ type Diagnostic struct { // Location is a source code location associated with the diagnostic message. // It may be zero if there is no associated location. Location dyn.Location + + Path dyn.Path } // Errorf creates a new error diagnostic. diff --git a/libs/dyn/convert/normalize.go b/libs/dyn/convert/normalize.go index f18b27fd24..33bc853e82 100644 --- a/libs/dyn/convert/normalize.go +++ b/libs/dyn/convert/normalize.go @@ -33,29 +33,29 @@ func Normalize(dst any, src dyn.Value, opts ...NormalizeOption) (dyn.Value, diag } } - return n.normalizeType(reflect.TypeOf(dst), src, []reflect.Type{}) + return n.normalizeType(reflect.TypeOf(dst), src, []reflect.Type{}, dyn.EmptyPath) } -func (n normalizeOptions) normalizeType(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeType(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) { for typ.Kind() == reflect.Pointer { typ = typ.Elem() } switch typ.Kind() { case reflect.Struct: - return n.normalizeStruct(typ, src, append(seen, typ)) + return n.normalizeStruct(typ, src, append(seen, typ), path) case reflect.Map: - return n.normalizeMap(typ, src, append(seen, typ)) + return n.normalizeMap(typ, src, append(seen, typ), path) case reflect.Slice: - return n.normalizeSlice(typ, src, append(seen, typ)) + return n.normalizeSlice(typ, src, append(seen, typ), path) case reflect.String: - return n.normalizeString(typ, src) + return n.normalizeString(typ, src, path) case reflect.Bool: - return n.normalizeBool(typ, src) + return n.normalizeBool(typ, src, path) case reflect.Int, reflect.Int32, reflect.Int64: - return n.normalizeInt(typ, src) + return n.normalizeInt(typ, src, path) case reflect.Float32, reflect.Float64: - return n.normalizeFloat(typ, src) + return n.normalizeFloat(typ, src, path) } return dyn.InvalidValue, diag.Errorf("unsupported type: %s", typ.Kind()) @@ -69,7 +69,7 @@ func typeMismatch(expected dyn.Kind, src dyn.Value) diag.Diagnostic { } } -func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics switch src.Kind() { @@ -83,14 +83,15 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen if !ok { diags = diags.Append(diag.Diagnostic{ Severity: diag.Warning, - Summary: fmt.Sprintf("unknown field: %s", pk.MustString()), + Summary: fmt.Sprintf("unknown field %q", pk.MustString()), Location: pk.Location(), + Path: path, }) continue } // Normalize the value according to the field type. - nv, err := n.normalizeType(typ.FieldByIndex(index).Type, pv, seen) + nv, err := n.normalizeType(typ.FieldByIndex(index).Type, pv, seen, path.Append(dyn.Key(pk.MustString()))) if err != nil { diags = diags.Extend(err) // Skip the element if it cannot be normalized. @@ -128,17 +129,17 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen var v dyn.Value switch ftyp.Kind() { case reflect.Struct, reflect.Map: - v, _ = n.normalizeType(ftyp, dyn.V(map[string]dyn.Value{}), seen) + v, _ = n.normalizeType(ftyp, dyn.V(map[string]dyn.Value{}), seen, path.Append(dyn.Key(k))) case reflect.Slice: - v, _ = n.normalizeType(ftyp, dyn.V([]dyn.Value{}), seen) + v, _ = n.normalizeType(ftyp, dyn.V([]dyn.Value{}), seen, path.Append(dyn.Key(k))) case reflect.String: - v, _ = n.normalizeType(ftyp, dyn.V(""), seen) + v, _ = n.normalizeType(ftyp, dyn.V(""), seen, path.Append(dyn.Key(k))) case reflect.Bool: - v, _ = n.normalizeType(ftyp, dyn.V(false), seen) + v, _ = n.normalizeType(ftyp, dyn.V(false), seen, path.Append(dyn.Key(k))) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - v, _ = n.normalizeType(ftyp, dyn.V(int64(0)), seen) + v, _ = n.normalizeType(ftyp, dyn.V(int64(0)), seen, path.Append(dyn.Key(k))) case reflect.Float32, reflect.Float64: - v, _ = n.normalizeType(ftyp, dyn.V(float64(0)), seen) + v, _ = n.normalizeType(ftyp, dyn.V(float64(0)), seen, path.Append(dyn.Key(k))) default: // Skip fields for which we do not have a natural [dyn.Value] equivalent. // For example, we don't handle reflect.Complex* and reflect.Uint* types. @@ -157,7 +158,7 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src)) } -func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics switch src.Kind() { @@ -168,7 +169,7 @@ func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []r pv := pair.Value // Normalize the value according to the map element type. - nv, err := n.normalizeType(typ.Elem(), pv, seen) + nv, err := n.normalizeType(typ.Elem(), pv, seen, path.Append(dyn.Key(pk.MustString()))) if err != nil { diags = diags.Extend(err) // Skip the element if it cannot be normalized. @@ -188,7 +189,7 @@ func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []r return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src)) } -func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics switch src.Kind() { @@ -196,7 +197,7 @@ func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen [ out := make([]dyn.Value, 0, len(src.MustSequence())) for _, v := range src.MustSequence() { // Normalize the value according to the slice element type. - v, err := n.normalizeType(typ.Elem(), v, seen) + v, err := n.normalizeType(typ.Elem(), v, seen, path.Append(dyn.Index(len(out)))) if err != nil { diags = diags.Extend(err) // Skip the element if it cannot be normalized. @@ -216,7 +217,7 @@ func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen [ return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindSequence, src)) } -func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics var out string @@ -236,7 +237,7 @@ func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value) (dyn. return dyn.NewValue(out, src.Location()), diags } -func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics var out bool @@ -266,7 +267,7 @@ func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value) (dyn.Va return dyn.NewValue(out, src.Location()), diags } -func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics var out int64 @@ -295,7 +296,7 @@ func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value) (dyn.Val return dyn.NewValue(out, src.Location()), diags } -func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics var out float64 diff --git a/libs/dyn/path.go b/libs/dyn/path.go index 76377e2dce..99905c55e8 100644 --- a/libs/dyn/path.go +++ b/libs/dyn/path.go @@ -93,6 +93,10 @@ func (p Path) HasPrefix(q Path) bool { func (p Path) String() string { var buf bytes.Buffer + if len(p) == 0 { + return "" + } + for i, c := range p { if i > 0 && c.key != "" { buf.WriteRune('.') From 96a2b39443783c4ceb1c8d74cf07f9ca55f97e8c Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 3 Apr 2024 09:22:32 +0200 Subject: [PATCH 2/7] Include dyn.Path in normalization warnings and errors --- libs/diag/diagnostic.go | 4 ++ libs/dyn/convert/normalize.go | 83 ++++++++++++++++-------------- libs/dyn/convert/normalize_test.go | 18 +++++++ 3 files changed, 66 insertions(+), 39 deletions(-) diff --git a/libs/diag/diagnostic.go b/libs/diag/diagnostic.go index 68b4ad611b..ddb3af3877 100644 --- a/libs/diag/diagnostic.go +++ b/libs/diag/diagnostic.go @@ -20,6 +20,10 @@ type Diagnostic struct { // Location is a source code location associated with the diagnostic message. // It may be zero if there is no associated location. Location dyn.Location + + // Path is a path to the value in a configuration tree that the diagnostic is associated with. + // It may be nil if there is no associated path. + Path dyn.Path } // Errorf creates a new error diagnostic. diff --git a/libs/dyn/convert/normalize.go b/libs/dyn/convert/normalize.go index ff4d94b888..7d6bde0516 100644 --- a/libs/dyn/convert/normalize.go +++ b/libs/dyn/convert/normalize.go @@ -33,51 +33,53 @@ func Normalize(dst any, src dyn.Value, opts ...NormalizeOption) (dyn.Value, diag } } - return n.normalizeType(reflect.TypeOf(dst), src, []reflect.Type{}) + return n.normalizeType(reflect.TypeOf(dst), src, []reflect.Type{}, dyn.EmptyPath) } -func (n normalizeOptions) normalizeType(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeType(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) { for typ.Kind() == reflect.Pointer { typ = typ.Elem() } switch typ.Kind() { case reflect.Struct: - return n.normalizeStruct(typ, src, append(seen, typ)) + return n.normalizeStruct(typ, src, append(seen, typ), path) case reflect.Map: - return n.normalizeMap(typ, src, append(seen, typ)) + return n.normalizeMap(typ, src, append(seen, typ), path) case reflect.Slice: - return n.normalizeSlice(typ, src, append(seen, typ)) + return n.normalizeSlice(typ, src, append(seen, typ), path) case reflect.String: - return n.normalizeString(typ, src) + return n.normalizeString(typ, src, path) case reflect.Bool: - return n.normalizeBool(typ, src) + return n.normalizeBool(typ, src, path) case reflect.Int, reflect.Int32, reflect.Int64: - return n.normalizeInt(typ, src) + return n.normalizeInt(typ, src, path) case reflect.Float32, reflect.Float64: - return n.normalizeFloat(typ, src) + return n.normalizeFloat(typ, src, path) } return dyn.InvalidValue, diag.Errorf("unsupported type: %s", typ.Kind()) } -func nullWarning(expected dyn.Kind, src dyn.Value) diag.Diagnostic { +func nullWarning(expected dyn.Kind, src dyn.Value, path dyn.Path) diag.Diagnostic { return diag.Diagnostic{ Severity: diag.Warning, Summary: fmt.Sprintf("expected a %s value, found null", expected), Location: src.Location(), + Path: path, } } -func typeMismatch(expected dyn.Kind, src dyn.Value) diag.Diagnostic { +func typeMismatch(expected dyn.Kind, src dyn.Value, path dyn.Path) diag.Diagnostic { return diag.Diagnostic{ Severity: diag.Error, Summary: fmt.Sprintf("expected %s, found %s", expected, src.Kind()), Location: src.Location(), + Path: path, } } -func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics switch src.Kind() { @@ -93,12 +95,13 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen Severity: diag.Warning, Summary: fmt.Sprintf("unknown field: %s", pk.MustString()), Location: pk.Location(), + Path: path, }) continue } // Normalize the value according to the field type. - nv, err := n.normalizeType(typ.FieldByIndex(index).Type, pv, seen) + nv, err := n.normalizeType(typ.FieldByIndex(index).Type, pv, seen, path.Append(dyn.Key(pk.MustString()))) if err != nil { diags = diags.Extend(err) // Skip the element if it cannot be normalized. @@ -136,17 +139,17 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen var v dyn.Value switch ftyp.Kind() { case reflect.Struct, reflect.Map: - v, _ = n.normalizeType(ftyp, dyn.V(map[string]dyn.Value{}), seen) + v, _ = n.normalizeType(ftyp, dyn.V(map[string]dyn.Value{}), seen, path.Append(dyn.Key(k))) case reflect.Slice: - v, _ = n.normalizeType(ftyp, dyn.V([]dyn.Value{}), seen) + v, _ = n.normalizeType(ftyp, dyn.V([]dyn.Value{}), seen, path.Append(dyn.Key(k))) case reflect.String: - v, _ = n.normalizeType(ftyp, dyn.V(""), seen) + v, _ = n.normalizeType(ftyp, dyn.V(""), seen, path.Append(dyn.Key(k))) case reflect.Bool: - v, _ = n.normalizeType(ftyp, dyn.V(false), seen) + v, _ = n.normalizeType(ftyp, dyn.V(false), seen, path.Append(dyn.Key(k))) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - v, _ = n.normalizeType(ftyp, dyn.V(int64(0)), seen) + v, _ = n.normalizeType(ftyp, dyn.V(int64(0)), seen, path.Append(dyn.Key(k))) case reflect.Float32, reflect.Float64: - v, _ = n.normalizeType(ftyp, dyn.V(float64(0)), seen) + v, _ = n.normalizeType(ftyp, dyn.V(float64(0)), seen, path.Append(dyn.Key(k))) default: // Skip fields for which we do not have a natural [dyn.Value] equivalent. // For example, we don't handle reflect.Complex* and reflect.Uint* types. @@ -162,10 +165,10 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen return src, diags } - return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src)) + return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src, path)) } -func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics switch src.Kind() { @@ -176,7 +179,7 @@ func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []r pv := pair.Value // Normalize the value according to the map element type. - nv, err := n.normalizeType(typ.Elem(), pv, seen) + nv, err := n.normalizeType(typ.Elem(), pv, seen, path.Append(dyn.Key(pk.MustString()))) if err != nil { diags = diags.Extend(err) // Skip the element if it cannot be normalized. @@ -193,10 +196,10 @@ func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []r return src, diags } - return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src)) + return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src, path)) } -func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics switch src.Kind() { @@ -204,7 +207,7 @@ func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen [ out := make([]dyn.Value, 0, len(src.MustSequence())) for _, v := range src.MustSequence() { // Normalize the value according to the slice element type. - v, err := n.normalizeType(typ.Elem(), v, seen) + v, err := n.normalizeType(typ.Elem(), v, seen, path.Append(dyn.Index(len(out)))) if err != nil { diags = diags.Extend(err) // Skip the element if it cannot be normalized. @@ -221,10 +224,10 @@ func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen [ return src, diags } - return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindSequence, src)) + return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindSequence, src, path)) } -func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics var out string @@ -239,15 +242,15 @@ func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value) (dyn. out = strconv.FormatFloat(src.MustFloat(), 'f', -1, 64) case dyn.KindNil: // Return a warning if the field is present but has a null value. - return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindString, src)) + return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindString, src, path)) default: - return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindString, src)) + return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindString, src, path)) } return dyn.NewValue(out, src.Location()), diags } -func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics var out bool @@ -268,19 +271,19 @@ func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value) (dyn.Va } // Cannot interpret as a boolean. - return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src)) + return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src, path)) } case dyn.KindNil: // Return a warning if the field is present but has a null value. - return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindBool, src)) + return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindBool, src, path)) default: - return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src)) + return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src, path)) } return dyn.NewValue(out, src.Location()), diags } -func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics var out int64 @@ -300,19 +303,20 @@ func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value) (dyn.Val Severity: diag.Error, Summary: fmt.Sprintf("cannot parse %q as an integer", src.MustString()), Location: src.Location(), + Path: path, }) } case dyn.KindNil: // Return a warning if the field is present but has a null value. - return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindInt, src)) + return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindInt, src, path)) default: - return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindInt, src)) + return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindInt, src, path)) } return dyn.NewValue(out, src.Location()), diags } -func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { var diags diag.Diagnostics var out float64 @@ -332,13 +336,14 @@ func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value) (dyn.V Severity: diag.Error, Summary: fmt.Sprintf("cannot parse %q as a floating point number", src.MustString()), Location: src.Location(), + Path: path, }) } case dyn.KindNil: // Return a warning if the field is present but has a null value. - return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindFloat, src)) + return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindFloat, src, path)) default: - return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindFloat, src)) + return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindFloat, src, path)) } return dyn.NewValue(out, src.Location()), diags diff --git a/libs/dyn/convert/normalize_test.go b/libs/dyn/convert/normalize_test.go index 66e781bb82..8567979689 100644 --- a/libs/dyn/convert/normalize_test.go +++ b/libs/dyn/convert/normalize_test.go @@ -43,6 +43,7 @@ func TestNormalizeStructElementDiagnostic(t *testing.T) { Severity: diag.Error, Summary: `expected string, found map`, Location: dyn.Location{}, + Path: dyn.NewPath(dyn.Key("bar")), }, err[0]) // Elements that encounter an error during normalization are dropped. @@ -68,6 +69,7 @@ func TestNormalizeStructUnknownField(t *testing.T) { Severity: diag.Warning, Summary: `unknown field: bar`, Location: vin.Get("foo").Location(), + Path: dyn.EmptyPath, }, err[0]) // The field that can be mapped to the struct field is retained. @@ -101,6 +103,7 @@ func TestNormalizeStructError(t *testing.T) { Severity: diag.Error, Summary: `expected map, found string`, Location: vin.Get("foo").Location(), + Path: dyn.EmptyPath, }, err[0]) } @@ -245,6 +248,7 @@ func TestNormalizeMapElementDiagnostic(t *testing.T) { Severity: diag.Error, Summary: `expected string, found map`, Location: dyn.Location{}, + Path: dyn.NewPath(dyn.Key("bar")), }, err[0]) // Elements that encounter an error during normalization are dropped. @@ -270,6 +274,7 @@ func TestNormalizeMapError(t *testing.T) { Severity: diag.Error, Summary: `expected map, found string`, Location: vin.Location(), + Path: dyn.EmptyPath, }, err[0]) } @@ -333,6 +338,7 @@ func TestNormalizeSliceElementDiagnostic(t *testing.T) { Severity: diag.Error, Summary: `expected string, found map`, Location: dyn.Location{}, + Path: dyn.NewPath(dyn.Index(2)), }, err[0]) // Elements that encounter an error during normalization are dropped. @@ -356,6 +362,7 @@ func TestNormalizeSliceError(t *testing.T) { Severity: diag.Error, Summary: `expected sequence, found string`, Location: vin.Location(), + Path: dyn.EmptyPath, }, err[0]) } @@ -410,6 +417,7 @@ func TestNormalizeStringNil(t *testing.T) { Severity: diag.Warning, Summary: `expected a string value, found null`, Location: vin.Location(), + Path: dyn.EmptyPath, }, err[0]) } @@ -446,6 +454,7 @@ func TestNormalizeStringError(t *testing.T) { Severity: diag.Error, Summary: `expected string, found map`, Location: dyn.Location{}, + Path: dyn.EmptyPath, }, err[0]) } @@ -466,6 +475,7 @@ func TestNormalizeBoolNil(t *testing.T) { Severity: diag.Warning, Summary: `expected a bool value, found null`, Location: vin.Location(), + Path: dyn.EmptyPath, }, err[0]) } @@ -507,6 +517,7 @@ func TestNormalizeBoolFromStringError(t *testing.T) { Severity: diag.Error, Summary: `expected bool, found string`, Location: vin.Location(), + Path: dyn.EmptyPath, }, err[0]) } @@ -519,6 +530,7 @@ func TestNormalizeBoolError(t *testing.T) { Severity: diag.Error, Summary: `expected bool, found map`, Location: dyn.Location{}, + Path: dyn.EmptyPath, }, err[0]) } @@ -539,6 +551,7 @@ func TestNormalizeIntNil(t *testing.T) { Severity: diag.Warning, Summary: `expected a int value, found null`, Location: vin.Location(), + Path: dyn.EmptyPath, }, err[0]) } @@ -567,6 +580,7 @@ func TestNormalizeIntFromStringError(t *testing.T) { Severity: diag.Error, Summary: `cannot parse "abc" as an integer`, Location: vin.Location(), + Path: dyn.EmptyPath, }, err[0]) } @@ -579,6 +593,7 @@ func TestNormalizeIntError(t *testing.T) { Severity: diag.Error, Summary: `expected int, found map`, Location: dyn.Location{}, + Path: dyn.EmptyPath, }, err[0]) } @@ -599,6 +614,7 @@ func TestNormalizeFloatNil(t *testing.T) { Severity: diag.Warning, Summary: `expected a float value, found null`, Location: vin.Location(), + Path: dyn.EmptyPath, }, err[0]) } @@ -627,6 +643,7 @@ func TestNormalizeFloatFromStringError(t *testing.T) { Severity: diag.Error, Summary: `cannot parse "abc" as a floating point number`, Location: vin.Location(), + Path: dyn.EmptyPath, }, err[0]) } @@ -639,5 +656,6 @@ func TestNormalizeFloatError(t *testing.T) { Severity: diag.Error, Summary: `expected float, found map`, Location: dyn.Location{}, + Path: dyn.EmptyPath, }, err[0]) } From 7df82a3b53307db061dab51e788a3d9e2e255a6f Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 3 Apr 2024 14:00:56 +0200 Subject: [PATCH 3/7] Work --- cmd/bundle/validate.go | 140 +++++++++++++++++++++++++++++++--------- libs/diag/diagnostic.go | 11 ++++ 2 files changed, 120 insertions(+), 31 deletions(-) diff --git a/cmd/bundle/validate.go b/cmd/bundle/validate.go index c42dc302cd..f5b602b6a7 100644 --- a/cmd/bundle/validate.go +++ b/cmd/bundle/validate.go @@ -1,30 +1,123 @@ package bundle import ( + "encoding/json" + "fmt" "path/filepath" + "strings" + "text/template" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/flags" + "github.com/fatih/color" "github.com/spf13/cobra" ) -// "red": color.RedString, -// "green": color.GreenString, -// "blue": color.BlueString, -// "yellow": color.YellowString, -// "magenta": color.MagentaString, -// "cyan": color.CyanString, +var validateFuncMap = template.FuncMap{ + "red": color.RedString, + "green": color.GreenString, + "blue": color.BlueString, + "yellow": color.YellowString, + "magenta": color.MagentaString, + "cyan": color.CyanString, + "bold": func(format string, a ...interface{}) string { + return color.New(color.Bold).Sprintf(format, a...) + }, + "italic": func(format string, a ...interface{}) string { + return color.New(color.Italic).Sprintf(format, a...) + }, +} -const tmpl = ` - {{ "Warning" | yellow }}: {{ .Summary }} +const errorTemplate = `{{ "Error" | red }}: {{ .Summary }} {{ "At " }}{{ .Path.String | green }} {{ "In " }}{{ .Location.String | cyan }} + ` +const warningTemplate = `{{ "Warning" | yellow }}: {{ .Summary }} + {{ "At " }}{{ .Path.String | green }} + {{ "In " }}{{ .Location.String | cyan }} + +` + +const summaryTemplate = `Name: {{ .Config.Bundle.Name | bold }} +Target: {{ .Config.Bundle.Target | bold }} +Workspace: + Host: {{ .Config.Workspace.Host | bold }} + User: {{ .Config.Workspace.CurrentUser.UserName | bold }} + Path: {{ .Config.Workspace.RootPath | bold }} + +{{ .Trailer }} +` + +func pluralize(n int, singular, plural string) string { + if n == 1 { + return fmt.Sprintf("%d %s", n, singular) + } + return fmt.Sprintf("%d %s", n, plural) +} + +func renderTextOutput(cmd *cobra.Command, b *bundle.Bundle, diags diag.Diagnostics) error { + // Print errors and warnings. + for _, d := range diags { + var t *template.Template + switch d.Severity { + case diag.Error: + t = template.Must(template.New("error").Funcs(validateFuncMap).Parse(errorTemplate)) + case diag.Warning: + t = template.Must(template.New("warning").Funcs(validateFuncMap).Parse(warningTemplate)) + } + + // Make file relative to bundle root + if d.Location.File != "" { + out, _ := filepath.Rel(b.RootPath, d.Location.File) + d.Location.File = out + } + + // Render the diagnostic with the appropriate template. + err := t.Execute(cmd.OutOrStdout(), d) + if err != nil { + return err + } + } + + trailer := []string{} + if errors := len(diags.Filter(diag.Error)); errors > 0 { + trailer = append(trailer, color.RedString(pluralize(errors, "error", "errors"))) + } + if warnings := len(diags.Filter(diag.Warning)); warnings > 0 { + trailer = append(trailer, color.YellowString(pluralize(warnings, "warning", "warnings"))) + } + if len(trailer) == 0 { + trailer = append(trailer, color.GreenString("Validation OK!")) + } + + // Print validation summary. + t := template.Must(template.New("summary").Funcs(validateFuncMap).Parse(summaryTemplate)) + err := t.Execute(cmd.OutOrStdout(), map[string]any{ + "Config": b.Config, + "Trailer": strings.Join(trailer, ", "), + }) + if err != nil { + return err + } + + return diags.Error() +} + +func renderJsonOutput(cmd *cobra.Command, b *bundle.Bundle, diags diag.Diagnostics) error { + buf, err := json.MarshalIndent(b.Config, "", " ") + if err != nil { + return err + } + cmd.OutOrStdout().Write(buf) + return diags.Error() +} + func newValidateCommand() *cobra.Command { cmd := &cobra.Command{ Use: "validate", @@ -44,29 +137,14 @@ func newValidateCommand() *cobra.Command { return err } - // Until we change up the output of this command to be a text representation, - // we'll just output all diagnostics as debug logs. - for _, d := range diags { - switch d.Severity { - case diag.Warning: - // Make file relative to bundle root - out, _ := filepath.Rel(b.RootPath, d.Location.File) - d.Location.File = out - err := cmdio.RenderWithTemplate(ctx, d, "", tmpl) - if err != nil { - return err - } - } - - // log.Debugf(cmd.Context(), "[%s]: %s", diag.Location, diag.Summary) + switch root.OutputType(cmd) { + case flags.OutputText: + return renderTextOutput(cmd, b, diags) + case flags.OutputJSON: + return renderJsonOutput(cmd, b, diags) + default: + return fmt.Errorf("unknown output type %s", root.OutputType(cmd)) } - - // buf, err := json.MarshalIndent(b.Config, "", " ") - // if err != nil { - // return err - // } - // cmd.OutOrStdout().Write(buf) - return nil } return cmd diff --git a/libs/diag/diagnostic.go b/libs/diag/diagnostic.go index ddb3af3877..6215275512 100644 --- a/libs/diag/diagnostic.go +++ b/libs/diag/diagnostic.go @@ -101,3 +101,14 @@ func (ds Diagnostics) Error() error { } return nil } + +// Filter returns a new list of diagnostics that match the specified severity. +func (ds Diagnostics) Filter(severity Severity) Diagnostics { + var out Diagnostics + for _, d := range ds { + if d.Severity == severity { + out = append(out, d) + } + } + return out +} From 55d9ef5eb0d87dd83e3977be81307de735b43c63 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 3 Apr 2024 14:03:09 +0200 Subject: [PATCH 4/7] Work --- bundle/tests/basic/databricks.yml | 11 ----------- libs/dyn/convert/normalize.go | 8 ++++---- libs/dyn/convert/normalize_test.go | 26 +++++++++++++------------- libs/dyn/path.go | 4 ---- 4 files changed, 17 insertions(+), 32 deletions(-) diff --git a/bundle/tests/basic/databricks.yml b/bundle/tests/basic/databricks.yml index 74bcfd35a3..de02d20bc6 100644 --- a/bundle/tests/basic/databricks.yml +++ b/bundle/tests/basic/databricks.yml @@ -1,13 +1,2 @@ bundle: name: basic - -foobar: unknown - -resources: - jobs: - my_job: - tasks: - - task_key: task_1 - - task_key: task_2 - - task_key: task_3 - task_property: unknown diff --git a/libs/dyn/convert/normalize.go b/libs/dyn/convert/normalize.go index df9fa69a2b..296e2abb26 100644 --- a/libs/dyn/convert/normalize.go +++ b/libs/dyn/convert/normalize.go @@ -72,7 +72,7 @@ func nullWarning(expected dyn.Kind, src dyn.Value, path dyn.Path) diag.Diagnosti func typeMismatch(expected dyn.Kind, src dyn.Value, path dyn.Path) diag.Diagnostic { return diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: fmt.Sprintf("expected %s, found %s", expected, src.Kind()), Location: src.Location(), Path: path, @@ -93,7 +93,7 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen if !ok { diags = diags.Append(diag.Diagnostic{ Severity: diag.Warning, - Summary: fmt.Sprintf("unknown field %q", pk.MustString()), + Summary: fmt.Sprintf("unknown field: %s", pk.MustString()), Location: pk.Location(), Path: path, }) @@ -300,7 +300,7 @@ func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value, path dyn } return dyn.InvalidValue, diags.Append(diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: fmt.Sprintf("cannot parse %q as an integer", src.MustString()), Location: src.Location(), Path: path, @@ -333,7 +333,7 @@ func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value, path d } return dyn.InvalidValue, diags.Append(diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: fmt.Sprintf("cannot parse %q as a floating point number", src.MustString()), Location: src.Location(), Path: path, diff --git a/libs/dyn/convert/normalize_test.go b/libs/dyn/convert/normalize_test.go index 8567979689..133eaef8fc 100644 --- a/libs/dyn/convert/normalize_test.go +++ b/libs/dyn/convert/normalize_test.go @@ -40,7 +40,7 @@ func TestNormalizeStructElementDiagnostic(t *testing.T) { vout, err := Normalize(typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: `expected string, found map`, Location: dyn.Location{}, Path: dyn.NewPath(dyn.Key("bar")), @@ -100,7 +100,7 @@ func TestNormalizeStructError(t *testing.T) { _, err := Normalize(typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: `expected map, found string`, Location: vin.Get("foo").Location(), Path: dyn.EmptyPath, @@ -245,7 +245,7 @@ func TestNormalizeMapElementDiagnostic(t *testing.T) { vout, err := Normalize(typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: `expected string, found map`, Location: dyn.Location{}, Path: dyn.NewPath(dyn.Key("bar")), @@ -271,7 +271,7 @@ func TestNormalizeMapError(t *testing.T) { _, err := Normalize(typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: `expected map, found string`, Location: vin.Location(), Path: dyn.EmptyPath, @@ -335,7 +335,7 @@ func TestNormalizeSliceElementDiagnostic(t *testing.T) { vout, err := Normalize(typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: `expected string, found map`, Location: dyn.Location{}, Path: dyn.NewPath(dyn.Index(2)), @@ -359,7 +359,7 @@ func TestNormalizeSliceError(t *testing.T) { _, err := Normalize(typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: `expected sequence, found string`, Location: vin.Location(), Path: dyn.EmptyPath, @@ -451,7 +451,7 @@ func TestNormalizeStringError(t *testing.T) { _, err := Normalize(&typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: `expected string, found map`, Location: dyn.Location{}, Path: dyn.EmptyPath, @@ -514,7 +514,7 @@ func TestNormalizeBoolFromStringError(t *testing.T) { _, err := Normalize(&typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: `expected bool, found string`, Location: vin.Location(), Path: dyn.EmptyPath, @@ -527,7 +527,7 @@ func TestNormalizeBoolError(t *testing.T) { _, err := Normalize(&typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: `expected bool, found map`, Location: dyn.Location{}, Path: dyn.EmptyPath, @@ -577,7 +577,7 @@ func TestNormalizeIntFromStringError(t *testing.T) { _, err := Normalize(&typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: `cannot parse "abc" as an integer`, Location: vin.Location(), Path: dyn.EmptyPath, @@ -590,7 +590,7 @@ func TestNormalizeIntError(t *testing.T) { _, err := Normalize(&typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: `expected int, found map`, Location: dyn.Location{}, Path: dyn.EmptyPath, @@ -640,7 +640,7 @@ func TestNormalizeFloatFromStringError(t *testing.T) { _, err := Normalize(&typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: `cannot parse "abc" as a floating point number`, Location: vin.Location(), Path: dyn.EmptyPath, @@ -653,7 +653,7 @@ func TestNormalizeFloatError(t *testing.T) { _, err := Normalize(&typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ - Severity: diag.Error, + Severity: diag.Warning, Summary: `expected float, found map`, Location: dyn.Location{}, Path: dyn.EmptyPath, diff --git a/libs/dyn/path.go b/libs/dyn/path.go index 99905c55e8..76377e2dce 100644 --- a/libs/dyn/path.go +++ b/libs/dyn/path.go @@ -93,10 +93,6 @@ func (p Path) HasPrefix(q Path) bool { func (p Path) String() string { var buf bytes.Buffer - if len(p) == 0 { - return "" - } - for i, c := range p { if i > 0 && c.key != "" { buf.WriteRune('.') From 47d27f1b77b00125c9bfe133fe06af70f033ffc8 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 3 Apr 2024 14:24:39 +0200 Subject: [PATCH 5/7] Work --- cmd/bundle/validate.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/bundle/validate.go b/cmd/bundle/validate.go index f5b602b6a7..4d5b44fff8 100644 --- a/cmd/bundle/validate.go +++ b/cmd/bundle/validate.go @@ -33,14 +33,14 @@ var validateFuncMap = template.FuncMap{ } const errorTemplate = `{{ "Error" | red }}: {{ .Summary }} - {{ "At " }}{{ .Path.String | green }} - {{ "In " }}{{ .Location.String | cyan }} + {{ "at " }}{{ .Path.String | green }} + {{ "in " }}{{ .Location.String | cyan }} ` const warningTemplate = `{{ "Warning" | yellow }}: {{ .Summary }} - {{ "At " }}{{ .Path.String | green }} - {{ "In " }}{{ .Location.String | cyan }} + {{ "at " }}{{ .Path.String | green }} + {{ "in " }}{{ .Location.String | cyan }} ` From 2741b15a5f7b01a5aeaf6089fd00e34ff8d1844f Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 3 Apr 2024 15:57:51 +0200 Subject: [PATCH 6/7] Trailer --- cmd/bundle/validate.go | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/cmd/bundle/validate.go b/cmd/bundle/validate.go index 4d5b44fff8..6b835466ac 100644 --- a/cmd/bundle/validate.go +++ b/cmd/bundle/validate.go @@ -61,6 +61,21 @@ func pluralize(n int, singular, plural string) string { return fmt.Sprintf("%d %s", n, plural) } +func buildTrailer(diags diag.Diagnostics) string { + parts := []string{} + if errors := len(diags.Filter(diag.Error)); errors > 0 { + parts = append(parts, color.RedString(pluralize(errors, "error", "errors"))) + } + if warnings := len(diags.Filter(diag.Warning)); warnings > 0 { + parts = append(parts, color.YellowString(pluralize(warnings, "warning", "warnings"))) + } + if len(parts) > 0 { + return fmt.Sprintf("Found %s", strings.Join(parts, " and ")) + } else { + return color.GreenString("Validation OK!") + } +} + func renderTextOutput(cmd *cobra.Command, b *bundle.Bundle, diags diag.Diagnostics) error { // Print errors and warnings. for _, d := range diags { @@ -85,22 +100,11 @@ func renderTextOutput(cmd *cobra.Command, b *bundle.Bundle, diags diag.Diagnosti } } - trailer := []string{} - if errors := len(diags.Filter(diag.Error)); errors > 0 { - trailer = append(trailer, color.RedString(pluralize(errors, "error", "errors"))) - } - if warnings := len(diags.Filter(diag.Warning)); warnings > 0 { - trailer = append(trailer, color.YellowString(pluralize(warnings, "warning", "warnings"))) - } - if len(trailer) == 0 { - trailer = append(trailer, color.GreenString("Validation OK!")) - } - // Print validation summary. t := template.Must(template.New("summary").Funcs(validateFuncMap).Parse(summaryTemplate)) err := t.Execute(cmd.OutOrStdout(), map[string]any{ "Config": b.Config, - "Trailer": strings.Join(trailer, ", "), + "Trailer": buildTrailer(diags), }) if err != nil { return err From 7e640c659ab00beb02c793b6c2898efa1c8a2137 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 3 Apr 2024 17:28:27 +0200 Subject: [PATCH 7/7] Pull template parsing out of loop --- cmd/bundle/validate.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/bundle/validate.go b/cmd/bundle/validate.go index 6b835466ac..4a04db4099 100644 --- a/cmd/bundle/validate.go +++ b/cmd/bundle/validate.go @@ -77,14 +77,17 @@ func buildTrailer(diags diag.Diagnostics) string { } func renderTextOutput(cmd *cobra.Command, b *bundle.Bundle, diags diag.Diagnostics) error { + errorT := template.Must(template.New("error").Funcs(validateFuncMap).Parse(errorTemplate)) + warningT := template.Must(template.New("warning").Funcs(validateFuncMap).Parse(warningTemplate)) + // Print errors and warnings. for _, d := range diags { var t *template.Template switch d.Severity { case diag.Error: - t = template.Must(template.New("error").Funcs(validateFuncMap).Parse(errorTemplate)) + t = errorT case diag.Warning: - t = template.Must(template.New("warning").Funcs(validateFuncMap).Parse(warningTemplate)) + t = warningT } // Make file relative to bundle root