diff --git a/cmd/bundle/validate.go b/cmd/bundle/validate.go index e625539b..4a04db40 100644 --- a/cmd/bundle/validate.go +++ b/cmd/bundle/validate.go @@ -2,15 +2,129 @@ 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/log" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/flags" + "github.com/fatih/color" "github.com/spf13/cobra" ) +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 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 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 { + 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 = errorT + case diag.Warning: + t = warningT + } + + // 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 + } + } + + // 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": buildTrailer(diags), + }) + 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", @@ -25,23 +139,19 @@ 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) + 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 ddb3af38..62152755 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 +}