Skip to content

Commit

Permalink
Make bundle validation print text output by default (#1335)
Browse files Browse the repository at this point in the history
## Changes

It now shows human-readable warnings and validation status.

## Tests

* Manual tests against many examples.
* Errors still return immediately.
  • Loading branch information
pietern authored Apr 3, 2024
1 parent b4e2645 commit 04cbc71
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 13 deletions.
136 changes: 123 additions & 13 deletions cmd/bundle/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)
}

buf, err := json.MarshalIndent(b.Config, "", " ")
if err != nil {
return err
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))
}
cmd.OutOrStdout().Write(buf)
return nil
}

return cmd
Expand Down
11 changes: 11 additions & 0 deletions libs/diag/diagnostic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 04cbc71

Please sign in to comment.