mirror of https://github.com/databricks/cli.git
Comments
This commit is contained in:
parent
bfa9c3a2c3
commit
1f26c68c04
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/databricks/cli/libs/dyn"
|
"github.com/databricks/cli/libs/dyn"
|
||||||
"github.com/databricks/cli/libs/dyn/yamlsaver"
|
"github.com/databricks/cli/libs/dyn/yamlsaver"
|
||||||
"github.com/databricks/cli/libs/textutil"
|
"github.com/databricks/cli/libs/textutil"
|
||||||
|
"github.com/databricks/databricks-sdk-go"
|
||||||
"github.com/databricks/databricks-sdk-go/apierr"
|
"github.com/databricks/databricks-sdk-go/apierr"
|
||||||
"github.com/databricks/databricks-sdk-go/service/dashboards"
|
"github.com/databricks/databricks-sdk-go/service/dashboards"
|
||||||
"github.com/databricks/databricks-sdk-go/service/workspace"
|
"github.com/databricks/databricks-sdk-go/service/workspace"
|
||||||
|
@ -33,8 +34,8 @@ import (
|
||||||
|
|
||||||
type dashboard struct {
|
type dashboard struct {
|
||||||
// Lookup flags for one-time generate.
|
// Lookup flags for one-time generate.
|
||||||
dashboardPath string
|
existingPath string
|
||||||
dashboardId string
|
existingID string
|
||||||
|
|
||||||
// Lookup flag for existing bundle resource.
|
// Lookup flag for existing bundle resource.
|
||||||
resource string
|
resource string
|
||||||
|
@ -55,9 +56,9 @@ type dashboard struct {
|
||||||
|
|
||||||
func (d *dashboard) resolveID(ctx context.Context, b *bundle.Bundle) (string, diag.Diagnostics) {
|
func (d *dashboard) resolveID(ctx context.Context, b *bundle.Bundle) (string, diag.Diagnostics) {
|
||||||
switch {
|
switch {
|
||||||
case d.dashboardPath != "":
|
case d.existingPath != "":
|
||||||
return d.resolveFromPath(ctx, b)
|
return d.resolveFromPath(ctx, b)
|
||||||
case d.dashboardId != "":
|
case d.existingID != "":
|
||||||
return d.resolveFromID(ctx, b)
|
return d.resolveFromID(ctx, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,10 +67,10 @@ func (d *dashboard) resolveID(ctx context.Context, b *bundle.Bundle) (string, di
|
||||||
|
|
||||||
func (d *dashboard) resolveFromPath(ctx context.Context, b *bundle.Bundle) (string, diag.Diagnostics) {
|
func (d *dashboard) resolveFromPath(ctx context.Context, b *bundle.Bundle) (string, diag.Diagnostics) {
|
||||||
w := b.WorkspaceClient()
|
w := b.WorkspaceClient()
|
||||||
obj, err := w.Workspace.GetStatusByPath(ctx, d.dashboardPath)
|
obj, err := w.Workspace.GetStatusByPath(ctx, d.existingPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if apierr.IsMissing(err) {
|
if apierr.IsMissing(err) {
|
||||||
return "", diag.Errorf("dashboard %q not found", path.Base(d.dashboardPath))
|
return "", diag.Errorf("dashboard %q not found", path.Base(d.existingPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit a more descriptive error message for legacy dashboards.
|
// Emit a more descriptive error message for legacy dashboards.
|
||||||
|
@ -77,7 +78,7 @@ func (d *dashboard) resolveFromPath(ctx context.Context, b *bundle.Bundle) (stri
|
||||||
return "", diag.Diagnostics{
|
return "", diag.Diagnostics{
|
||||||
{
|
{
|
||||||
Severity: diag.Error,
|
Severity: diag.Error,
|
||||||
Summary: fmt.Sprintf("dashboard %q is a legacy dashboard", path.Base(d.dashboardPath)),
|
Summary: fmt.Sprintf("dashboard %q is a legacy dashboard", path.Base(d.existingPath)),
|
||||||
Detail: "" +
|
Detail: "" +
|
||||||
"Databricks Asset Bundles work exclusively with AI/BI dashboards.\n" +
|
"Databricks Asset Bundles work exclusively with AI/BI dashboards.\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
|
@ -114,10 +115,10 @@ func (d *dashboard) resolveFromPath(ctx context.Context, b *bundle.Bundle) (stri
|
||||||
|
|
||||||
func (d *dashboard) resolveFromID(ctx context.Context, b *bundle.Bundle) (string, diag.Diagnostics) {
|
func (d *dashboard) resolveFromID(ctx context.Context, b *bundle.Bundle) (string, diag.Diagnostics) {
|
||||||
w := b.WorkspaceClient()
|
w := b.WorkspaceClient()
|
||||||
obj, err := w.Lakeview.GetByDashboardId(ctx, d.dashboardId)
|
obj, err := w.Lakeview.GetByDashboardId(ctx, d.existingID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if apierr.IsMissing(err) {
|
if apierr.IsMissing(err) {
|
||||||
return "", diag.Errorf("dashboard with ID %s not found", d.dashboardId)
|
return "", diag.Errorf("dashboard with ID %s not found", d.existingID)
|
||||||
}
|
}
|
||||||
return "", diag.FromErr(err)
|
return "", diag.FromErr(err)
|
||||||
}
|
}
|
||||||
|
@ -234,7 +235,32 @@ func (d *dashboard) saveConfiguration(ctx context.Context, b *bundle.Bundle, das
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboard) generateForResource(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
func waitForChanges(ctx context.Context, w *databricks.WorkspaceClient, dashboard *dashboards.Dashboard) diag.Diagnostics {
|
||||||
|
// Compute [time.Time] for the most recent update.
|
||||||
|
tref, err := time.Parse(time.RFC3339, dashboard.UpdateTime)
|
||||||
|
if err != nil {
|
||||||
|
return diag.FromErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
obj, err := w.Workspace.GetStatusByPath(ctx, dashboard.Path)
|
||||||
|
if err != nil {
|
||||||
|
return diag.FromErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute [time.Time] from timestamp in millis since epoch.
|
||||||
|
tcur := time.Unix(0, obj.ModifiedAt*int64(time.Millisecond))
|
||||||
|
if tcur.After(tref) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dashboard) updateDashboardForResource(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
resource, ok := b.Config.Resources.Dashboards[d.resource]
|
resource, ok := b.Config.Resources.Dashboards[d.resource]
|
||||||
if !ok {
|
if !ok {
|
||||||
return diag.Errorf("dashboard resource %q is not defined", d.resource)
|
return diag.Errorf("dashboard resource %q is not defined", d.resource)
|
||||||
|
@ -250,10 +276,11 @@ func (d *dashboard) generateForResource(ctx context.Context, b *bundle.Bundle) d
|
||||||
// Overwrite the dashboard at the path referenced from the resource.
|
// Overwrite the dashboard at the path referenced from the resource.
|
||||||
dashboardPath := resource.FilePath
|
dashboardPath := resource.FilePath
|
||||||
|
|
||||||
|
w := b.WorkspaceClient()
|
||||||
|
|
||||||
// Start polling the underlying dashboard for changes.
|
// Start polling the underlying dashboard for changes.
|
||||||
var etag string
|
var etag string
|
||||||
for {
|
for {
|
||||||
w := b.WorkspaceClient()
|
|
||||||
dashboard, err := w.Lakeview.GetByDashboardId(ctx, dashboardID)
|
dashboard, err := w.Lakeview.GetByDashboardId(ctx, dashboardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return diag.FromErr(err)
|
return diag.FromErr(err)
|
||||||
|
@ -274,28 +301,11 @@ func (d *dashboard) generateForResource(ctx context.Context, b *bundle.Bundle) d
|
||||||
// Update the etag for the next iteration.
|
// Update the etag for the next iteration.
|
||||||
etag = dashboard.Etag
|
etag = dashboard.Etag
|
||||||
|
|
||||||
// Compute [time.Time] for the most recent update.
|
|
||||||
tref, err := time.Parse(time.RFC3339, dashboard.UpdateTime)
|
|
||||||
if err != nil {
|
|
||||||
return diag.FromErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now poll the workspace API for changes.
|
// Now poll the workspace API for changes.
|
||||||
// This is much more efficient than polling the dashboard API.
|
// This is much more efficient than polling the dashboard API because it
|
||||||
for {
|
// includes the entire serialized dashboard whereas we're only interested
|
||||||
obj, err := w.Workspace.GetStatusByPath(ctx, dashboard.Path)
|
// in the last modified time of the dashboard here.
|
||||||
if err != nil {
|
waitForChanges(ctx, w, dashboard)
|
||||||
return diag.FromErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute [time.Time] from timestamp in millis since epoch.
|
|
||||||
tcur := time.Unix(0, obj.ModifiedAt*int64(time.Millisecond))
|
|
||||||
if tcur.After(tref) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,7 +356,7 @@ func (d *dashboard) runForResource(ctx context.Context, b *bundle.Bundle) diag.D
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
return d.generateForResource(ctx, b)
|
return d.updateDashboardForResource(ctx, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboard) runForExisting(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
func (d *dashboard) runForExisting(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
|
@ -419,22 +429,30 @@ func NewGenerateDashboardCommand() *cobra.Command {
|
||||||
d := &dashboard{}
|
d := &dashboard{}
|
||||||
|
|
||||||
// Lookup flags.
|
// Lookup flags.
|
||||||
cmd.Flags().StringVar(&d.dashboardPath, "existing-path", "", `workspace path of the dashboard to generate configuration for`)
|
cmd.Flags().StringVar(&d.existingPath, "existing-path", "", `workspace path of the dashboard to generate configuration for`)
|
||||||
cmd.Flags().StringVar(&d.dashboardId, "existing-id", "", `ID of the dashboard to generate configuration for`)
|
cmd.Flags().StringVar(&d.existingID, "existing-id", "", `ID of the dashboard to generate configuration for`)
|
||||||
cmd.Flags().StringVar(&d.resource, "resource", "", `resource key of dashboard to watch for changes`)
|
cmd.Flags().StringVar(&d.resource, "resource", "", `resource key of dashboard to watch for changes`)
|
||||||
|
|
||||||
|
// Alias lookup flags that include the resource type name.
|
||||||
|
// Included for symmetry with the other generate commands, but we prefer the shorter flags.
|
||||||
|
cmd.Flags().StringVar(&d.existingPath, "existing-dashboard-path", "", `workspace path of the dashboard to generate configuration for`)
|
||||||
|
cmd.Flags().StringVar(&d.existingID, "existing-dashboard-id", "", `ID of the dashboard to generate configuration for`)
|
||||||
|
cmd.Flags().MarkHidden("existing-dashboard-path")
|
||||||
|
cmd.Flags().MarkHidden("existing-dashboard-id")
|
||||||
|
|
||||||
// Output flags.
|
// Output flags.
|
||||||
cmd.Flags().StringVarP(&d.resourceDir, "resource-dir", "d", "./resources", `directory to write the configuration to`)
|
cmd.Flags().StringVarP(&d.resourceDir, "resource-dir", "d", "./resources", `directory to write the configuration to`)
|
||||||
cmd.Flags().StringVarP(&d.dashboardDir, "dashboard-dir", "s", "./src", `directory to write the dashboard representation to`)
|
cmd.Flags().StringVarP(&d.dashboardDir, "dashboard-dir", "s", "./src", `directory to write the dashboard representation to`)
|
||||||
cmd.Flags().BoolVarP(&d.force, "force", "f", false, `force overwrite existing files in the output directory`)
|
cmd.Flags().BoolVarP(&d.force, "force", "f", false, `force overwrite existing files in the output directory`)
|
||||||
|
|
||||||
|
// Exactly one of the lookup flags must be provided.
|
||||||
cmd.MarkFlagsOneRequired(
|
cmd.MarkFlagsOneRequired(
|
||||||
"existing-path",
|
"existing-path",
|
||||||
"existing-id",
|
"existing-id",
|
||||||
"resource",
|
"resource",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Watch flags.
|
// Watch flag. This is relevant only in combination with the resource flag.
|
||||||
cmd.Flags().BoolVar(&d.watch, "watch", false, `watch for changes to the dashboard and update the configuration`)
|
cmd.Flags().BoolVar(&d.watch, "watch", false, `watch for changes to the dashboard and update the configuration`)
|
||||||
|
|
||||||
// Make sure the watch flag is only used with the existing-resource flag.
|
// Make sure the watch flag is only used with the existing-resource flag.
|
||||||
|
|
|
@ -22,7 +22,7 @@ func TestDashboard_ErrorOnLegacyDashboard(t *testing.T) {
|
||||||
// < }
|
// < }
|
||||||
|
|
||||||
d := dashboard{
|
d := dashboard{
|
||||||
dashboardPath: "/path/to/legacy dashboard",
|
existingPath: "/path/to/legacy dashboard",
|
||||||
}
|
}
|
||||||
|
|
||||||
m := mocks.NewMockWorkspaceClient(t)
|
m := mocks.NewMockWorkspaceClient(t)
|
||||||
|
|
Loading…
Reference in New Issue