databricks-cli/bundle/render/render_text_output.go

231 lines
5.9 KiB
Go

package render
import (
"context"
"fmt"
"io"
"path/filepath"
"sort"
"strings"
"text/template"
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/libs/cmdio"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/databricks-sdk-go/service/iam"
"github.com/fatih/color"
)
var renderFuncMap = 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 summaryHeaderTemplate = `{{- if .Name -}}
Name: {{ .Name | bold }}
{{- if .Target }}
Target: {{ .Target | bold }}
{{- end }}
{{- if or .User .Host .Path }}
Workspace:
{{- if .Host }}
Host: {{ .Host | bold }}
{{- end }}
{{- if .User }}
User: {{ .User | bold }}
{{- end }}
{{- if .Path }}
Path: {{ .Path | bold }}
{{- end }}
{{- end }}
{{ end -}}`
const resourcesTemplate = `Resources:
{{- range . }}
{{ .GroupName }}:
{{- range .Resources }}
{{ .Key | bold }}:
Name: {{ .Name }}
URL: {{ if .URL }}{{ .URL | cyan }}{{ else }}{{ "(not deployed)" | cyan }}{{ end }}
{{- end }}
{{- end }}
`
type ResourceGroup struct {
GroupName string
Resources []ResourceInfo
}
type ResourceInfo struct {
Key string
Name string
URL string
}
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 recommendations := len(diags.Filter(diag.Recommendation)); recommendations > 0 {
parts = append(parts, color.BlueString(pluralize(recommendations, "recommendation", "recommendations")))
}
switch {
case len(parts) >= 3:
first := strings.Join(parts[:len(parts)-1], ", ")
last := parts[len(parts)-1]
return fmt.Sprintf("Found %s, and %s\n", first, last)
case len(parts) == 2:
return fmt.Sprintf("Found %s and %s\n", parts[0], parts[1])
case len(parts) == 1:
return fmt.Sprintf("Found %s\n", parts[0])
default:
// No diagnostics to print.
return color.GreenString("Validation OK!\n")
}
}
func renderSummaryHeaderTemplate(out io.Writer, b *bundle.Bundle) error {
if b == nil {
return renderSummaryHeaderTemplate(out, &bundle.Bundle{})
}
var currentUser = &iam.User{}
if b.Config.Workspace.CurrentUser != nil {
if b.Config.Workspace.CurrentUser.User != nil {
currentUser = b.Config.Workspace.CurrentUser.User
}
}
t := template.Must(template.New("summary").Funcs(renderFuncMap).Parse(summaryHeaderTemplate))
err := t.Execute(out, map[string]any{
"Name": b.Config.Bundle.Name,
"Target": b.Config.Bundle.Target,
"User": currentUser.UserName,
"Path": b.Config.Workspace.RootPath,
"Host": b.Config.Workspace.Host,
})
return err
}
func renderDiagnosticsOnly(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics) error {
for _, d := range diags {
for i := range d.Locations {
if b == nil {
break
}
// Make location relative to bundle root
if d.Locations[i].File != "" {
out, err := filepath.Rel(b.BundleRootPath, d.Locations[i].File)
// if we can't relativize the path, just use path as-is
if err == nil {
d.Locations[i].File = out
}
}
}
}
return cmdio.RenderDiagnostics(out, diags)
}
// RenderOptions contains options for rendering diagnostics.
type RenderOptions struct {
// variable to include leading new line
RenderSummaryTable bool
}
// RenderDiagnostics renders the diagnostics in a human-readable format.
func RenderDiagnostics(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics, opts RenderOptions) error {
err := renderDiagnosticsOnly(out, b, diags)
if err != nil {
return fmt.Errorf("failed to render diagnostics: %w", err)
}
if opts.RenderSummaryTable {
if b != nil {
err = renderSummaryHeaderTemplate(out, b)
if err != nil {
return fmt.Errorf("failed to render summary: %w", err)
}
io.WriteString(out, "\n")
}
trailer := buildTrailer(diags)
io.WriteString(out, trailer)
}
return nil
}
func RenderSummary(ctx context.Context, out io.Writer, b *bundle.Bundle) error {
if err := renderSummaryHeaderTemplate(out, b); err != nil {
return err
}
var resourceGroups []ResourceGroup
for _, group := range b.Config.Resources.AllResources() {
resources := make([]ResourceInfo, 0, len(group.Resources))
for key, resource := range group.Resources {
resources = append(resources, ResourceInfo{
Key: key,
Name: resource.GetName(),
URL: resource.GetURL(),
})
}
if len(resources) > 0 {
resourceGroups = append(resourceGroups, ResourceGroup{
GroupName: group.Description.PluralTitle,
Resources: resources,
})
}
}
if err := renderResourcesTemplate(out, resourceGroups); err != nil {
return fmt.Errorf("failed to render resources template: %w", err)
}
return nil
}
// Helper function to sort and render resource groups using the template
func renderResourcesTemplate(out io.Writer, resourceGroups []ResourceGroup) error {
// Sort everything to ensure consistent output
sort.Slice(resourceGroups, func(i, j int) bool {
return resourceGroups[i].GroupName < resourceGroups[j].GroupName
})
for _, group := range resourceGroups {
sort.Slice(group.Resources, func(i, j int) bool {
return group.Resources[i].Key < group.Resources[j].Key
})
}
t := template.Must(template.New("resources").Funcs(renderFuncMap).Parse(resourcesTemplate))
return t.Execute(out, resourceGroups)
}