mirror of https://github.com/databricks/cli.git
Add a textual bundle summary command
This commit is contained in:
parent
84b47745e4
commit
869637dc54
|
@ -0,0 +1,62 @@
|
|||
package mutator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/libs/diag"
|
||||
)
|
||||
|
||||
type initializeUrls struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// InitializeURLs makes sure the URL field of each resource is configured.
|
||||
// NOTE: since this depends on an extra API call, this mutator adds some extra
|
||||
// latency. As such, it should only be used when needed.
|
||||
// This URL field is used for the output of the 'bundle summary' CLI command.
|
||||
func InitializeURLs() bundle.Mutator {
|
||||
return &initializeUrls{}
|
||||
}
|
||||
|
||||
func (m *initializeUrls) Name() string {
|
||||
return fmt.Sprintf("ConfigureURLs(%s)", m.name)
|
||||
}
|
||||
|
||||
func (m *initializeUrls) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||
workspaceId, err := b.WorkspaceClient().CurrentWorkspaceID(ctx)
|
||||
orgId := strconv.FormatInt(workspaceId, 10)
|
||||
if err != nil {
|
||||
return diag.FromErr(err)
|
||||
}
|
||||
configureForOrgId(b, orgId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func configureForOrgId(b *bundle.Bundle, orgId string) {
|
||||
urlPrefix := b.Config.Workspace.Host
|
||||
if !strings.HasSuffix(urlPrefix, "/") {
|
||||
urlPrefix += "/"
|
||||
}
|
||||
urlSuffix := ""
|
||||
|
||||
// Add ?o=<workspace id> only if <workspace id> wasn't in the subdomain already.
|
||||
// The ?o= is needed when vanity URLs / legacy workspace URLs are used.
|
||||
// If it's not needed we prefer to leave it out since these URLs are rather
|
||||
// long for most terminals.
|
||||
//
|
||||
// See https://docs.databricks.com/en/workspace/workspace-details.html for
|
||||
// further reading about the '?o=' suffix.
|
||||
if !strings.Contains(urlPrefix, orgId) {
|
||||
urlSuffix = "?o=" + orgId
|
||||
}
|
||||
|
||||
for _, rs := range b.Config.Resources.AllResources() {
|
||||
for _, r := range rs {
|
||||
r.InitializeURL(urlPrefix, urlSuffix)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package mutator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/databricks-sdk-go/service/catalog"
|
||||
"github.com/databricks/databricks-sdk-go/service/jobs"
|
||||
"github.com/databricks/databricks-sdk-go/service/ml"
|
||||
"github.com/databricks/databricks-sdk-go/service/pipelines"
|
||||
"github.com/databricks/databricks-sdk-go/service/serving"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIntitializeURLs(t *testing.T) {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Workspace: config.Workspace{
|
||||
Host: "https://mycompany.databricks.com/",
|
||||
},
|
||||
Resources: config.Resources{
|
||||
Jobs: map[string]*resources.Job{
|
||||
"job1": {
|
||||
ID: "1",
|
||||
JobSettings: &jobs.JobSettings{Name: "job1"},
|
||||
},
|
||||
},
|
||||
Pipelines: map[string]*resources.Pipeline{
|
||||
"pipeline1": {
|
||||
ID: "3",
|
||||
PipelineSpec: &pipelines.PipelineSpec{Name: "pipeline1"},
|
||||
},
|
||||
},
|
||||
Experiments: map[string]*resources.MlflowExperiment{
|
||||
"experiment1": {
|
||||
ID: "4",
|
||||
Experiment: &ml.Experiment{Name: "experiment1"},
|
||||
},
|
||||
},
|
||||
Models: map[string]*resources.MlflowModel{
|
||||
"model1": {
|
||||
ID: "6",
|
||||
Model: &ml.Model{Name: "model1"},
|
||||
},
|
||||
},
|
||||
ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{
|
||||
"servingendpoint1": {
|
||||
ID: "7",
|
||||
CreateServingEndpoint: &serving.CreateServingEndpoint{
|
||||
Name: "my_serving_endpoint",
|
||||
},
|
||||
},
|
||||
},
|
||||
RegisteredModels: map[string]*resources.RegisteredModel{
|
||||
"registeredmodel1": {
|
||||
ID: "8",
|
||||
CreateRegisteredModelRequest: &catalog.CreateRegisteredModelRequest{
|
||||
Name: "my_registered_model",
|
||||
},
|
||||
},
|
||||
},
|
||||
QualityMonitors: map[string]*resources.QualityMonitor{
|
||||
"qualityMonitor1": {
|
||||
CreateMonitor: &catalog.CreateMonitor{
|
||||
TableName: "catalog.schema.qualityMonitor1",
|
||||
},
|
||||
},
|
||||
},
|
||||
Schemas: map[string]*resources.Schema{
|
||||
"schema1": {
|
||||
ID: "catalog.schema",
|
||||
CreateSchema: &catalog.CreateSchema{
|
||||
Name: "schema",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedURLs := map[string]string{
|
||||
"job1": "https://mycompany.databricks.com/jobs/1?o=123456",
|
||||
"pipeline1": "https://mycompany.databricks.com/pipelines/3?o=123456",
|
||||
"experiment1": "https://mycompany.databricks.com/ml/experiments/4?o=123456",
|
||||
"model1": "https://mycompany.databricks.com/ml/models/model1?o=123456",
|
||||
"servingendpoint1": "https://mycompany.databricks.com/ml/endpoints/my_serving_endpoint?o=123456",
|
||||
"registeredmodel1": "https://mycompany.databricks.com/explore/data/models/8?o=123456",
|
||||
"qualityMonitor1": "https://mycompany.databricks.com/explore/data/catalog/schema/qualityMonitor1?o=123456",
|
||||
"schema1": "https://mycompany.databricks.com/explore/data/catalog/schema?o=123456",
|
||||
}
|
||||
|
||||
configureForOrgId(b, "123456")
|
||||
|
||||
for _, rs := range b.Config.Resources.AllResources() {
|
||||
for key, r := range rs {
|
||||
require.Equal(t, expectedURLs[key], r.GetURL(), "Unexpected URL for "+key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntitializeURLs_NoOrgId(t *testing.T) {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Workspace: config.Workspace{
|
||||
// Hostname with org id in URL and no trailing /
|
||||
Host: "https://adb-123456.azuredatabricks.net",
|
||||
},
|
||||
Resources: config.Resources{
|
||||
Jobs: map[string]*resources.Job{
|
||||
"job1": {
|
||||
ID: "1",
|
||||
JobSettings: &jobs.JobSettings{Name: "job1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
configureForOrgId(b, "123456")
|
||||
|
||||
require.Equal(t, "https://adb-123456.azuredatabricks.net/jobs/1", b.Config.Resources.Jobs["job1"].URL)
|
||||
}
|
|
@ -29,6 +29,69 @@ type ConfigResource interface {
|
|||
// Terraform equivalent name of the resource. For example "databricks_job"
|
||||
// for jobs and "databricks_pipeline" for pipelines.
|
||||
TerraformResourceName() string
|
||||
|
||||
// GetName returns the in-product name of the resource.
|
||||
GetName() string
|
||||
|
||||
// GetURL returns the URL of the resource.
|
||||
GetURL() string
|
||||
|
||||
// InitializeURL initializes the URL field of the resource.
|
||||
InitializeURL(urlPrefix string, urlSuffix string)
|
||||
}
|
||||
|
||||
func (r *Resources) AllResources() map[string]map[string]ConfigResource {
|
||||
result := make(map[string]map[string]ConfigResource)
|
||||
|
||||
jobResources := make(map[string]ConfigResource)
|
||||
for key, job := range r.Jobs {
|
||||
jobResources[key] = job
|
||||
}
|
||||
result["jobs"] = jobResources
|
||||
|
||||
pipelineResources := make(map[string]ConfigResource)
|
||||
for key, pipeline := range r.Pipelines {
|
||||
pipelineResources[key] = pipeline
|
||||
}
|
||||
result["pipelines"] = pipelineResources
|
||||
|
||||
modelResources := make(map[string]ConfigResource)
|
||||
for key, model := range r.Models {
|
||||
modelResources[key] = model
|
||||
}
|
||||
result["models"] = modelResources
|
||||
|
||||
experimentResources := make(map[string]ConfigResource)
|
||||
for key, experiment := range r.Experiments {
|
||||
experimentResources[key] = experiment
|
||||
}
|
||||
result["experiments"] = experimentResources
|
||||
|
||||
modelServingEndpointResources := make(map[string]ConfigResource)
|
||||
for key, endpoint := range r.ModelServingEndpoints {
|
||||
modelServingEndpointResources[key] = endpoint
|
||||
}
|
||||
result["model_serving_endpoints"] = modelServingEndpointResources
|
||||
|
||||
registeredModelResources := make(map[string]ConfigResource)
|
||||
for key, registeredModel := range r.RegisteredModels {
|
||||
registeredModelResources[key] = registeredModel
|
||||
}
|
||||
result["registered_models"] = registeredModelResources
|
||||
|
||||
qualityMonitorResources := make(map[string]ConfigResource)
|
||||
for key, qualityMonitor := range r.QualityMonitors {
|
||||
qualityMonitorResources[key] = qualityMonitor
|
||||
}
|
||||
result["quality_monitors"] = qualityMonitorResources
|
||||
|
||||
schemaResources := make(map[string]ConfigResource)
|
||||
for key, schema := range r.Schemas {
|
||||
schemaResources[key] = schema
|
||||
}
|
||||
result["schemas"] = schemaResources
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *Resources) FindResourceByConfigKey(key string) (ConfigResource, error) {
|
||||
|
|
|
@ -14,6 +14,7 @@ type Job struct {
|
|||
ID string `json:"id,omitempty" bundle:"readonly"`
|
||||
Permissions []Permission `json:"permissions,omitempty"`
|
||||
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
|
||||
URL string `json:"url,omitempty" bundle:"internal"`
|
||||
|
||||
*jobs.JobSettings
|
||||
}
|
||||
|
@ -44,3 +45,18 @@ func (j *Job) Exists(ctx context.Context, w *databricks.WorkspaceClient, id stri
|
|||
func (j *Job) TerraformResourceName() string {
|
||||
return "databricks_job"
|
||||
}
|
||||
|
||||
func (j *Job) InitializeURL(urlPrefix string, urlSuffix string) {
|
||||
if j.ID == "" {
|
||||
return
|
||||
}
|
||||
j.URL = urlPrefix + "jobs/" + j.ID + urlSuffix
|
||||
}
|
||||
|
||||
func (j *Job) GetName() string {
|
||||
return j.Name
|
||||
}
|
||||
|
||||
func (j *Job) GetURL() string {
|
||||
return j.URL
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ type MlflowExperiment struct {
|
|||
ID string `json:"id,omitempty" bundle:"readonly"`
|
||||
Permissions []Permission `json:"permissions,omitempty"`
|
||||
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
|
||||
URL string `json:"url,omitempty" bundle:"internal"`
|
||||
|
||||
*ml.Experiment
|
||||
}
|
||||
|
@ -39,3 +40,18 @@ func (s *MlflowExperiment) Exists(ctx context.Context, w *databricks.WorkspaceCl
|
|||
func (s *MlflowExperiment) TerraformResourceName() string {
|
||||
return "databricks_mlflow_experiment"
|
||||
}
|
||||
|
||||
func (s *MlflowExperiment) InitializeURL(urlPrefix string, urlSuffix string) {
|
||||
if s.ID == "" {
|
||||
return
|
||||
}
|
||||
s.URL = urlPrefix + "ml/experiments/" + s.ID + urlSuffix
|
||||
}
|
||||
|
||||
func (s *MlflowExperiment) GetName() string {
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *MlflowExperiment) GetURL() string {
|
||||
return s.URL
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ type MlflowModel struct {
|
|||
ID string `json:"id,omitempty" bundle:"readonly"`
|
||||
Permissions []Permission `json:"permissions,omitempty"`
|
||||
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
|
||||
URL string `json:"url,omitempty" bundle:"internal"`
|
||||
|
||||
*ml.Model
|
||||
}
|
||||
|
@ -39,3 +40,18 @@ func (s *MlflowModel) Exists(ctx context.Context, w *databricks.WorkspaceClient,
|
|||
func (s *MlflowModel) TerraformResourceName() string {
|
||||
return "databricks_mlflow_model"
|
||||
}
|
||||
|
||||
func (s *MlflowModel) InitializeURL(urlPrefix string, urlSuffix string) {
|
||||
if s.ID == "" {
|
||||
return
|
||||
}
|
||||
s.URL = urlPrefix + "ml/models/" + s.Name + urlSuffix
|
||||
}
|
||||
|
||||
func (s *MlflowModel) GetName() string {
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *MlflowModel) GetURL() string {
|
||||
return s.URL
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ type ModelServingEndpoint struct {
|
|||
Permissions []Permission `json:"permissions,omitempty"`
|
||||
|
||||
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
|
||||
URL string `json:"url,omitempty" bundle:"internal"`
|
||||
}
|
||||
|
||||
func (s *ModelServingEndpoint) UnmarshalJSON(b []byte) error {
|
||||
|
@ -47,3 +48,18 @@ func (s *ModelServingEndpoint) Exists(ctx context.Context, w *databricks.Workspa
|
|||
func (s *ModelServingEndpoint) TerraformResourceName() string {
|
||||
return "databricks_model_serving"
|
||||
}
|
||||
|
||||
func (s *ModelServingEndpoint) InitializeURL(urlPrefix string, urlSuffix string) {
|
||||
if s.ID == "" {
|
||||
return
|
||||
}
|
||||
s.URL = urlPrefix + "ml/endpoints/" + s.Name + urlSuffix
|
||||
}
|
||||
|
||||
func (s *ModelServingEndpoint) GetName() string {
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *ModelServingEndpoint) GetURL() string {
|
||||
return s.URL
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ type Pipeline struct {
|
|||
ID string `json:"id,omitempty" bundle:"readonly"`
|
||||
Permissions []Permission `json:"permissions,omitempty"`
|
||||
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
|
||||
URL string `json:"url,omitempty" bundle:"internal"`
|
||||
|
||||
*pipelines.PipelineSpec
|
||||
}
|
||||
|
@ -39,3 +40,18 @@ func (p *Pipeline) Exists(ctx context.Context, w *databricks.WorkspaceClient, id
|
|||
func (p *Pipeline) TerraformResourceName() string {
|
||||
return "databricks_pipeline"
|
||||
}
|
||||
|
||||
func (p *Pipeline) InitializeURL(urlPrefix string, urlSuffix string) {
|
||||
if p.ID == "" {
|
||||
return
|
||||
}
|
||||
p.URL = urlPrefix + "pipelines/" + p.ID + urlSuffix
|
||||
}
|
||||
|
||||
func (p *Pipeline) GetName() string {
|
||||
return p.Name
|
||||
}
|
||||
|
||||
func (s *Pipeline) GetURL() string {
|
||||
return s.URL
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package resources
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/databricks/cli/libs/log"
|
||||
"github.com/databricks/databricks-sdk-go"
|
||||
|
@ -20,6 +21,7 @@ type QualityMonitor struct {
|
|||
ID string `json:"id,omitempty" bundle:"readonly"`
|
||||
|
||||
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
|
||||
URL string `json:"url,omitempty" bundle:"internal"`
|
||||
}
|
||||
|
||||
func (s *QualityMonitor) UnmarshalJSON(b []byte) error {
|
||||
|
@ -44,3 +46,18 @@ func (s *QualityMonitor) Exists(ctx context.Context, w *databricks.WorkspaceClie
|
|||
func (s *QualityMonitor) TerraformResourceName() string {
|
||||
return "databricks_quality_monitor"
|
||||
}
|
||||
|
||||
func (s *QualityMonitor) InitializeURL(urlPrefix string, urlSuffix string) {
|
||||
if s.TableName == "" {
|
||||
return
|
||||
}
|
||||
s.URL = urlPrefix + "explore/data/" + strings.ReplaceAll(s.TableName, ".", "/") + urlSuffix
|
||||
}
|
||||
|
||||
func (s *QualityMonitor) GetName() string {
|
||||
return s.TableName
|
||||
}
|
||||
|
||||
func (s *QualityMonitor) GetURL() string {
|
||||
return s.URL
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package resources
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/databricks/cli/libs/log"
|
||||
"github.com/databricks/databricks-sdk-go"
|
||||
|
@ -24,6 +25,7 @@ type RegisteredModel struct {
|
|||
*catalog.CreateRegisteredModelRequest
|
||||
|
||||
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
|
||||
URL string `json:"url,omitempty" bundle:"internal"`
|
||||
}
|
||||
|
||||
func (s *RegisteredModel) UnmarshalJSON(b []byte) error {
|
||||
|
@ -48,3 +50,18 @@ func (s *RegisteredModel) Exists(ctx context.Context, w *databricks.WorkspaceCli
|
|||
func (s *RegisteredModel) TerraformResourceName() string {
|
||||
return "databricks_registered_model"
|
||||
}
|
||||
|
||||
func (s *RegisteredModel) InitializeURL(urlPrefix string, urlSuffix string) {
|
||||
if s.ID == "" {
|
||||
return
|
||||
}
|
||||
s.URL = urlPrefix + "explore/data/models/" + strings.ReplaceAll(s.ID, ".", "/") + urlSuffix
|
||||
}
|
||||
|
||||
func (s *RegisteredModel) GetName() string {
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *RegisteredModel) GetURL() string {
|
||||
return s.URL
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/databricks/databricks-sdk-go"
|
||||
"github.com/databricks/databricks-sdk-go/marshal"
|
||||
"github.com/databricks/databricks-sdk-go/service/catalog"
|
||||
)
|
||||
|
@ -16,6 +21,30 @@ type Schema struct {
|
|||
*catalog.CreateSchema
|
||||
|
||||
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
|
||||
URL string `json:"url,omitempty" bundle:"internal"`
|
||||
}
|
||||
|
||||
func (s *Schema) Exists(ctx context.Context, w *databricks.WorkspaceClient, id string) (bool, error) {
|
||||
return false, fmt.Errorf("schema.Exists() is not supported")
|
||||
}
|
||||
|
||||
func (s *Schema) TerraformResourceName() string {
|
||||
return "databricks_schema"
|
||||
}
|
||||
|
||||
func (s *Schema) InitializeURL(urlPrefix string, urlSuffix string) {
|
||||
if s.ID == "" {
|
||||
return
|
||||
}
|
||||
s.URL = urlPrefix + "explore/data/" + strings.ReplaceAll(s.ID, ".", "/") + urlSuffix
|
||||
}
|
||||
|
||||
func (s *Schema) GetURL() string {
|
||||
return s.URL
|
||||
}
|
||||
|
||||
func (s *Schema) GetName() string {
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *Schema) UnmarshalJSON(b []byte) error {
|
||||
|
|
|
@ -3,6 +3,7 @@ package config
|
|||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -61,3 +62,22 @@ func TestCustomMarshallerIsImplemented(t *testing.T) {
|
|||
}, "Resource %s does not have a custom unmarshaller", field.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourcesAllResourcesCompleteness(t *testing.T) {
|
||||
r := Resources{}
|
||||
rt := reflect.TypeOf(r)
|
||||
|
||||
result := r.AllResources()
|
||||
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
field := rt.Field(i)
|
||||
jsonTag := field.Tag.Get("json")
|
||||
|
||||
if idx := strings.Index(jsonTag, ","); idx != -1 {
|
||||
jsonTag = jsonTag[:idx]
|
||||
}
|
||||
|
||||
_, exists := result[jsonTag]
|
||||
assert.True(t, exists, "Field %s is missing in AllResources map", field.Name)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
|
@ -56,7 +58,7 @@ const warningTemplate = `{{ "Warning" | yellow }}: {{ .Summary }}
|
|||
|
||||
`
|
||||
|
||||
const summaryTemplate = `{{- if .Name -}}
|
||||
const summaryHeaderTemplate = `{{- if .Name -}}
|
||||
Name: {{ .Name | bold }}
|
||||
{{- if .Target }}
|
||||
Target: {{ .Target | bold }}
|
||||
|
@ -73,12 +75,30 @@ Workspace:
|
|||
Path: {{ .Path | bold }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{ end -}}`
|
||||
|
||||
{{ end -}}
|
||||
|
||||
{{ .Trailer }}
|
||||
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)
|
||||
|
@ -95,15 +115,15 @@ func buildTrailer(diags diag.Diagnostics) string {
|
|||
parts = append(parts, color.YellowString(pluralize(warnings, "warning", "warnings")))
|
||||
}
|
||||
if len(parts) > 0 {
|
||||
return fmt.Sprintf("Found %s", strings.Join(parts, " and "))
|
||||
return fmt.Sprintf("Found %s\n", strings.Join(parts, " and "))
|
||||
} else {
|
||||
return color.GreenString("Validation OK!")
|
||||
return color.GreenString("Validation OK!\n")
|
||||
}
|
||||
}
|
||||
|
||||
func renderSummaryTemplate(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics) error {
|
||||
func renderSummaryHeaderTemplate(out io.Writer, b *bundle.Bundle) error {
|
||||
if b == nil {
|
||||
return renderSummaryTemplate(out, &bundle.Bundle{}, diags)
|
||||
return renderSummaryHeaderTemplate(out, &bundle.Bundle{})
|
||||
}
|
||||
|
||||
var currentUser = &iam.User{}
|
||||
|
@ -114,20 +134,19 @@ func renderSummaryTemplate(out io.Writer, b *bundle.Bundle, diags diag.Diagnosti
|
|||
}
|
||||
}
|
||||
|
||||
t := template.Must(template.New("summary").Funcs(renderFuncMap).Parse(summaryTemplate))
|
||||
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,
|
||||
"Trailer": buildTrailer(diags),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func renderDiagnostics(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics) error {
|
||||
func renderDiagnosticsOnly(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics) error {
|
||||
errorT := template.Must(template.New("error").Funcs(renderFuncMap).Parse(errorTemplate))
|
||||
warningT := template.Must(template.New("warning").Funcs(renderFuncMap).Parse(warningTemplate))
|
||||
|
||||
|
@ -173,19 +192,74 @@ type RenderOptions struct {
|
|||
RenderSummaryTable bool
|
||||
}
|
||||
|
||||
// RenderTextOutput renders the diagnostics in a human-readable format.
|
||||
func RenderTextOutput(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics, opts RenderOptions) error {
|
||||
err := renderDiagnostics(out, b, diags)
|
||||
// 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 {
|
||||
err = renderSummaryTemplate(out, b, diags)
|
||||
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, 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)
|
||||
}
|
||||
|
|
|
@ -2,14 +2,19 @@ package render
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/cli/libs/diag"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
assert "github.com/databricks/cli/libs/dyn/dynassert"
|
||||
"github.com/databricks/databricks-sdk-go/service/catalog"
|
||||
"github.com/databricks/databricks-sdk-go/service/iam"
|
||||
"github.com/databricks/databricks-sdk-go/service/jobs"
|
||||
"github.com/databricks/databricks-sdk-go/service/pipelines"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -192,10 +197,10 @@ func TestRenderTextOutput(t *testing.T) {
|
|||
t.Run(tc.name, func(t *testing.T) {
|
||||
writer := &bytes.Buffer{}
|
||||
|
||||
err := RenderTextOutput(writer, tc.bundle, tc.diags, tc.opts)
|
||||
err := RenderDiagnostics(writer, tc.bundle, tc.diags, tc.opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.expected, writer.String())
|
||||
require.Equal(t, tc.expected, writer.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -310,10 +315,10 @@ func TestRenderDiagnostics(t *testing.T) {
|
|||
t.Run(tc.name, func(t *testing.T) {
|
||||
writer := &bytes.Buffer{}
|
||||
|
||||
err := renderDiagnostics(writer, bundle, tc.diags)
|
||||
err := renderDiagnosticsOnly(writer, bundle, tc.diags)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.expected, writer.String())
|
||||
require.Equal(t, tc.expected, writer.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -321,8 +326,92 @@ func TestRenderDiagnostics(t *testing.T) {
|
|||
func TestRenderSummaryTemplate_nilBundle(t *testing.T) {
|
||||
writer := &bytes.Buffer{}
|
||||
|
||||
err := renderSummaryTemplate(writer, nil, nil)
|
||||
err := renderSummaryHeaderTemplate(writer, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Validation OK!\n", writer.String())
|
||||
io.WriteString(writer, buildTrailer(nil))
|
||||
|
||||
require.Equal(t, "Validation OK!\n", writer.String())
|
||||
}
|
||||
|
||||
func TestRenderSummary(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a mock bundle with various resources
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Bundle: config.Bundle{
|
||||
Name: "test-bundle",
|
||||
Target: "test-target",
|
||||
},
|
||||
Workspace: config.Workspace{
|
||||
Host: "https://mycompany.databricks.com/",
|
||||
},
|
||||
Resources: config.Resources{
|
||||
Jobs: map[string]*resources.Job{
|
||||
"job1": {
|
||||
ID: "1",
|
||||
URL: "https://url1",
|
||||
JobSettings: &jobs.JobSettings{Name: "job1-name"},
|
||||
},
|
||||
"job2": {
|
||||
ID: "2",
|
||||
URL: "https://url2",
|
||||
JobSettings: &jobs.JobSettings{Name: "job2-name"},
|
||||
},
|
||||
},
|
||||
Pipelines: map[string]*resources.Pipeline{
|
||||
"pipeline2": {
|
||||
ID: "4",
|
||||
// no URL
|
||||
PipelineSpec: &pipelines.PipelineSpec{Name: "pipeline2-name"},
|
||||
},
|
||||
"pipeline1": {
|
||||
ID: "3",
|
||||
URL: "https://url3",
|
||||
PipelineSpec: &pipelines.PipelineSpec{Name: "pipeline1-name"},
|
||||
},
|
||||
},
|
||||
Schemas: map[string]*resources.Schema{
|
||||
"schema1": {
|
||||
ID: "catalog.schema",
|
||||
CreateSchema: &catalog.CreateSchema{
|
||||
Name: "schema",
|
||||
},
|
||||
// no URL
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
writer := &bytes.Buffer{}
|
||||
err := RenderSummary(ctx, writer, b)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedSummary := `Name: test-bundle
|
||||
Target: test-target
|
||||
Workspace:
|
||||
Host: https://mycompany.databricks.com/
|
||||
Resources:
|
||||
Jobs:
|
||||
job1:
|
||||
Name: job1-name
|
||||
URL: https://url1
|
||||
job2:
|
||||
Name: job2-name
|
||||
URL: https://url2
|
||||
Pipelines:
|
||||
pipeline1:
|
||||
Name: pipeline1-name
|
||||
URL: https://url3
|
||||
pipeline2:
|
||||
Name: pipeline2-name
|
||||
URL: (not deployed)
|
||||
Schemas:
|
||||
schema1:
|
||||
Name: schema
|
||||
URL: (not deployed)
|
||||
`
|
||||
require.Equal(t, expectedSummary, writer.String())
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ func newDeployCommand() *cobra.Command {
|
|||
}
|
||||
|
||||
renderOpts := render.RenderOptions{RenderSummaryTable: false}
|
||||
err := render.RenderTextOutput(cmd.OutOrStdout(), b, diags, renderOpts)
|
||||
err := render.RenderDiagnostics(cmd.OutOrStdout(), b, diags, renderOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render output: %w", err)
|
||||
}
|
||||
|
|
|
@ -8,8 +8,10 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/config/mutator"
|
||||
"github.com/databricks/cli/bundle/deploy/terraform"
|
||||
"github.com/databricks/cli/bundle/phases"
|
||||
"github.com/databricks/cli/bundle/render"
|
||||
"github.com/databricks/cli/cmd/bundle/utils"
|
||||
"github.com/databricks/cli/cmd/root"
|
||||
"github.com/databricks/cli/libs/flags"
|
||||
|
@ -19,11 +21,8 @@ import (
|
|||
func newSummaryCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "summary",
|
||||
Short: "Describe the bundle resources and their deployment states",
|
||||
Short: "Summarize resources deployed by this bundle",
|
||||
Args: root.NoArgs,
|
||||
|
||||
// This command is currently intended for the Databricks VSCode extension only
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
var forcePull bool
|
||||
|
@ -60,14 +59,15 @@ func newSummaryCommand() *cobra.Command {
|
|||
}
|
||||
}
|
||||
|
||||
diags = bundle.Apply(ctx, b, terraform.Load())
|
||||
diags = bundle.Apply(ctx, b,
|
||||
bundle.Seq(terraform.Load(), mutator.InitializeURLs()))
|
||||
if err := diags.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch root.OutputType(cmd) {
|
||||
case flags.OutputText:
|
||||
return fmt.Errorf("%w, only json output is supported", errors.ErrUnsupported)
|
||||
return render.RenderSummary(ctx, cmd.OutOrStdout(), b)
|
||||
case flags.OutputJSON:
|
||||
buf, err := json.MarshalIndent(b.Config, "", " ")
|
||||
if err != nil {
|
||||
|
|
|
@ -54,7 +54,7 @@ func newValidateCommand() *cobra.Command {
|
|||
switch root.OutputType(cmd) {
|
||||
case flags.OutputText:
|
||||
renderOpts := render.RenderOptions{RenderSummaryTable: true}
|
||||
err := render.RenderTextOutput(cmd.OutOrStdout(), b, diags, renderOpts)
|
||||
err := render.RenderDiagnostics(cmd.OutOrStdout(), b, diags, renderOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render output: %w", err)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue