2024-07-01 09:01:10 +00:00
|
|
|
package render
|
|
|
|
|
|
|
|
import (
|
2024-08-24 18:48:36 +00:00
|
|
|
"context"
|
2024-07-01 09:01:10 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"path/filepath"
|
2024-08-24 18:48:36 +00:00
|
|
|
"sort"
|
2024-07-01 09:01:10 +00:00
|
|
|
"strings"
|
|
|
|
"text/template"
|
|
|
|
|
|
|
|
"github.com/databricks/cli/bundle"
|
|
|
|
"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 errorTemplate = `{{ "Error" | red }}: {{ .Summary }}
|
2024-07-25 15:16:27 +00:00
|
|
|
{{- range $index, $element := .Paths }}
|
|
|
|
{{ if eq $index 0 }}at {{else}} {{ end}}{{ $element.String | green }}
|
2024-07-01 09:01:10 +00:00
|
|
|
{{- end }}
|
2024-07-23 17:20:11 +00:00
|
|
|
{{- range $index, $element := .Locations }}
|
|
|
|
{{ if eq $index 0 }}in {{else}} {{ end}}{{ $element.String | cyan }}
|
2024-07-01 09:01:10 +00:00
|
|
|
{{- end }}
|
|
|
|
{{- if .Detail }}
|
|
|
|
|
|
|
|
{{ .Detail }}
|
|
|
|
{{- end }}
|
|
|
|
|
|
|
|
`
|
|
|
|
|
|
|
|
const warningTemplate = `{{ "Warning" | yellow }}: {{ .Summary }}
|
2024-07-25 15:16:27 +00:00
|
|
|
{{- range $index, $element := .Paths }}
|
|
|
|
{{ if eq $index 0 }}at {{else}} {{ end}}{{ $element.String | green }}
|
2024-07-01 09:01:10 +00:00
|
|
|
{{- end }}
|
2024-07-23 17:20:11 +00:00
|
|
|
{{- range $index, $element := .Locations }}
|
|
|
|
{{ if eq $index 0 }}in {{else}} {{ end}}{{ $element.String | cyan }}
|
2024-07-01 09:01:10 +00:00
|
|
|
{{- end }}
|
|
|
|
{{- if .Detail }}
|
|
|
|
|
|
|
|
{{ .Detail }}
|
|
|
|
{{- end }}
|
|
|
|
|
|
|
|
`
|
|
|
|
|
2024-08-24 18:48:36 +00:00
|
|
|
const summaryHeaderTemplate = `{{- if .Name -}}
|
2024-07-01 09:01:10 +00:00
|
|
|
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 }}
|
2024-08-24 18:48:36 +00:00
|
|
|
{{ end -}}`
|
2024-07-01 09:01:10 +00:00
|
|
|
|
2024-08-24 18:48:36 +00:00
|
|
|
const resourcesTemplate = `Resources:
|
|
|
|
{{- range . }}
|
|
|
|
{{ .GroupName }}:
|
|
|
|
{{- range .Resources }}
|
|
|
|
{{ .Key | bold }}:
|
|
|
|
Name: {{ .Name }}
|
|
|
|
URL: {{ if .URL }}{{ .URL | cyan }}{{ else }}{{ "(not deployed)" | cyan }}{{ end }}
|
|
|
|
{{- end }}
|
|
|
|
{{- end }}
|
2024-07-01 09:01:10 +00:00
|
|
|
`
|
|
|
|
|
2024-08-24 18:48:36 +00:00
|
|
|
type ResourceGroup struct {
|
|
|
|
GroupName string
|
|
|
|
Resources []ResourceInfo
|
|
|
|
}
|
|
|
|
|
|
|
|
type ResourceInfo struct {
|
|
|
|
Key string
|
|
|
|
Name string
|
|
|
|
URL string
|
|
|
|
}
|
|
|
|
|
2024-07-01 09:01:10 +00:00
|
|
|
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 {
|
2024-08-24 18:48:36 +00:00
|
|
|
return fmt.Sprintf("Found %s\n", strings.Join(parts, " and "))
|
2024-07-01 09:01:10 +00:00
|
|
|
} else {
|
2024-08-24 18:48:36 +00:00
|
|
|
return color.GreenString("Validation OK!\n")
|
2024-07-01 09:01:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-24 18:48:36 +00:00
|
|
|
func renderSummaryHeaderTemplate(out io.Writer, b *bundle.Bundle) error {
|
2024-07-01 09:01:10 +00:00
|
|
|
if b == nil {
|
2024-08-24 18:48:36 +00:00
|
|
|
return renderSummaryHeaderTemplate(out, &bundle.Bundle{})
|
2024-07-01 09:01:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var currentUser = &iam.User{}
|
|
|
|
|
|
|
|
if b.Config.Workspace.CurrentUser != nil {
|
|
|
|
if b.Config.Workspace.CurrentUser.User != nil {
|
|
|
|
currentUser = b.Config.Workspace.CurrentUser.User
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-24 18:48:36 +00:00
|
|
|
t := template.Must(template.New("summary").Funcs(renderFuncMap).Parse(summaryHeaderTemplate))
|
2024-07-01 09:01:10 +00:00
|
|
|
err := t.Execute(out, map[string]any{
|
2024-08-24 18:48:36 +00:00
|
|
|
"Name": b.Config.Bundle.Name,
|
|
|
|
"Target": b.Config.Bundle.Target,
|
|
|
|
"User": currentUser.UserName,
|
|
|
|
"Path": b.Config.Workspace.RootPath,
|
|
|
|
"Host": b.Config.Workspace.Host,
|
2024-07-01 09:01:10 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-08-24 18:48:36 +00:00
|
|
|
func renderDiagnosticsOnly(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics) error {
|
2024-07-01 09:01:10 +00:00
|
|
|
errorT := template.Must(template.New("error").Funcs(renderFuncMap).Parse(errorTemplate))
|
|
|
|
warningT := template.Must(template.New("warning").Funcs(renderFuncMap).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
|
|
|
|
}
|
|
|
|
|
2024-07-23 17:20:11 +00:00
|
|
|
for i := range d.Locations {
|
|
|
|
if b == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make location relative to bundle root
|
|
|
|
if d.Locations[i].File != "" {
|
2024-09-27 10:03:05 +00:00
|
|
|
out, err := filepath.Rel(b.BundleRootPath, d.Locations[i].File)
|
2024-07-23 17:20:11 +00:00
|
|
|
// if we can't relativize the path, just use path as-is
|
|
|
|
if err == nil {
|
|
|
|
d.Locations[i].File = out
|
|
|
|
}
|
2024-07-01 09:01:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Render the diagnostic with the appropriate template.
|
|
|
|
err := t.Execute(out, d)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to render template: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-07-10 11:14:57 +00:00
|
|
|
// RenderOptions contains options for rendering diagnostics.
|
|
|
|
type RenderOptions struct {
|
|
|
|
// variable to include leading new line
|
|
|
|
|
|
|
|
RenderSummaryTable bool
|
|
|
|
}
|
|
|
|
|
2024-08-24 18:48:36 +00:00
|
|
|
// 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)
|
2024-07-01 09:01:10 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to render diagnostics: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-07-10 11:14:57 +00:00
|
|
|
if opts.RenderSummaryTable {
|
2024-08-24 18:48:36 +00:00
|
|
|
if b != nil {
|
|
|
|
err = renderSummaryHeaderTemplate(out, b)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to render summary: %w", err)
|
|
|
|
}
|
|
|
|
io.WriteString(out, "\n")
|
2024-07-10 11:14:57 +00:00
|
|
|
}
|
2024-08-24 18:48:36 +00:00
|
|
|
trailer := buildTrailer(diags)
|
|
|
|
io.WriteString(out, trailer)
|
2024-07-01 09:01:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2024-08-24 18:48:36 +00:00
|
|
|
|
|
|
|
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, r := range b.Config.Resources.AllResources() {
|
|
|
|
resources := make([]ResourceInfo, 0, len(r))
|
|
|
|
for key, resource := range r {
|
|
|
|
resources = append(resources, ResourceInfo{
|
|
|
|
Key: key,
|
|
|
|
Name: resource.GetName(),
|
|
|
|
URL: resource.GetURL(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(resources) > 0 {
|
|
|
|
capitalizedGroup := strings.ToUpper(group[:1]) + group[1:]
|
|
|
|
resourceGroups = append(resourceGroups, ResourceGroup{
|
|
|
|
GroupName: capitalizedGroup,
|
|
|
|
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)
|
|
|
|
}
|