-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
1,056 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package generate | ||
|
||
import ( | ||
"github.com/databricks/cli/libs/dyn" | ||
"github.com/databricks/databricks-sdk-go/service/dashboards" | ||
) | ||
|
||
func ConvertDashboardToValue(dashboard *dashboards.Dashboard, filePath string) (dyn.Value, error) { | ||
// The majority of fields of the dashboard struct are read-only. | ||
// We copy the relevant fields manually. | ||
dv := map[string]dyn.Value{ | ||
"display_name": dyn.NewValue(dashboard.DisplayName, []dyn.Location{{Line: 1}}), | ||
"parent_path": dyn.NewValue("${workspace.file_path}", []dyn.Location{{Line: 2}}), | ||
"warehouse_id": dyn.NewValue(dashboard.WarehouseId, []dyn.Location{{Line: 3}}), | ||
"definition_path": dyn.NewValue(filePath, []dyn.Location{{Line: 4}}), | ||
} | ||
|
||
return dyn.V(dv), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,281 @@ | ||
package generate | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"path" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
|
||
"github.com/databricks/cli/bundle" | ||
"github.com/databricks/cli/bundle/config/generate" | ||
"github.com/databricks/cli/bundle/deploy/terraform" | ||
"github.com/databricks/cli/bundle/phases" | ||
"github.com/databricks/cli/cmd/root" | ||
"github.com/databricks/cli/libs/diag" | ||
"github.com/databricks/cli/libs/dyn" | ||
"github.com/databricks/cli/libs/dyn/yamlsaver" | ||
"github.com/databricks/cli/libs/textutil" | ||
"github.com/databricks/databricks-sdk-go" | ||
"github.com/databricks/databricks-sdk-go/service/dashboards" | ||
"github.com/databricks/databricks-sdk-go/service/workspace" | ||
"github.com/spf13/cobra" | ||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
type dashboard struct { | ||
resourceDir string | ||
dashboardDir string | ||
force bool | ||
|
||
// Relative path from the resource directory to the dashboard directory. | ||
relativeDir string | ||
|
||
existingDashboardPath string | ||
existingDashboardId string | ||
watch string | ||
|
||
key string | ||
} | ||
|
||
func (d *dashboard) resolveDashboardID(ctx context.Context, w *databricks.WorkspaceClient) diag.Diagnostics { | ||
if d.existingDashboardPath == "" { | ||
return nil | ||
} | ||
|
||
obj, err := w.Workspace.GetStatusByPath(ctx, d.existingDashboardPath) | ||
if err != nil { | ||
return diag.FromErr(err) | ||
} | ||
|
||
if obj.ObjectType != workspace.ObjectTypeDashboard { | ||
found := strings.ToLower(obj.ObjectType.String()) | ||
return diag.Diagnostics{ | ||
{ | ||
Severity: diag.Error, | ||
Summary: fmt.Sprintf("expected a dashboard, found a %s", found), | ||
Locations: []dyn.Location{{ | ||
File: d.existingDashboardPath, | ||
}}, | ||
}, | ||
} | ||
} | ||
|
||
if obj.ResourceId == "" { | ||
return diag.Diagnostics{ | ||
{ | ||
Severity: diag.Error, | ||
Summary: "expected resource ID to be set", | ||
Locations: []dyn.Location{{ | ||
File: d.existingDashboardPath, | ||
}}, | ||
}, | ||
} | ||
} | ||
|
||
d.existingDashboardId = obj.ResourceId | ||
return nil | ||
} | ||
|
||
func (d *dashboard) saveConfiguration(ctx context.Context, dashboard *dashboards.Dashboard) error { | ||
// TODO: add flag | ||
key := d.key | ||
if key == "" { | ||
key = textutil.NormalizeString(dashboard.DisplayName) | ||
} | ||
|
||
dashboardFilePath := path.Join(d.relativeDir, fmt.Sprintf("%s.lvdash.json", key)) | ||
v, err := generate.ConvertDashboardToValue(dashboard, dashboardFilePath) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
result := map[string]dyn.Value{ | ||
"resources": dyn.V(map[string]dyn.Value{ | ||
"dashboards": dyn.V(map[string]dyn.Value{ | ||
key: v, | ||
}), | ||
}), | ||
} | ||
|
||
// Make sure the output directory exists. | ||
if err := os.MkdirAll(d.resourceDir, 0755); err != nil { | ||
return err | ||
} | ||
|
||
filename := filepath.Join(d.resourceDir, fmt.Sprintf("%s.yml", key)) | ||
saver := yamlsaver.NewSaverWithStyle(map[string]yaml.Style{ | ||
"display_name": yaml.DoubleQuotedStyle, | ||
}) | ||
err = saver.SaveAsYAML(result, filename, false) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (d *dashboard) remarshal(data []byte) ([]byte, error) { | ||
var tmp any | ||
var err error | ||
err = json.Unmarshal(data, &tmp) | ||
if err != nil { | ||
return nil, err | ||
} | ||
out, err := json.MarshalIndent(tmp, "", " ") | ||
if err != nil { | ||
return nil, err | ||
} | ||
return out, nil | ||
} | ||
|
||
func (d *dashboard) saveSerializedDashboard(ctx context.Context, dashboard *dashboards.Dashboard, dst string) error { | ||
// Unmarshal and remarshal the serialized dashboard to ensure it is formatted correctly. | ||
// The result will have alphabetically sorted keys and be indented. | ||
data, err := d.remarshal([]byte(dashboard.SerializedDashboard)) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Make sure the output directory exists. | ||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { | ||
return err | ||
} | ||
|
||
return os.WriteFile(dst, data, 0644) | ||
} | ||
|
||
func (d *dashboard) watchForChanges(ctx context.Context, b *bundle.Bundle) error { | ||
diags := bundle.Apply(ctx, b, bundle.Seq( | ||
phases.Initialize(), | ||
terraform.Interpolate(), | ||
terraform.Write(), | ||
terraform.StatePull(), | ||
terraform.Load(terraform.ErrorOnEmptyState), | ||
)) | ||
if err := diags.Error(); err != nil { | ||
return err | ||
} | ||
|
||
dash, ok := b.Config.Resources.Dashboards[d.watch] | ||
if !ok { | ||
return fmt.Errorf("dashboard %s not found", d.watch) | ||
} | ||
|
||
// fmt.Println(dash.DefinitionPath) | ||
|
||
w := b.WorkspaceClient() | ||
etag := "" | ||
|
||
cwd, err := os.Getwd() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
relPath, err := filepath.Rel(cwd, dash.DefinitionPath) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
for { | ||
dashboard, err := w.Lakeview.GetByDashboardId(ctx, dash.ID) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// fmt.Println(dashboard.Path) | ||
// fmt.Println(dashboard.Etag) | ||
// fmt.Println(dashboard.UpdateTime) | ||
|
||
// obj, err := w.Workspace.GetStatusByPath(ctx, "/Users/[email protected]/.bundle/dashboard-eng-work-generate/dev/files/[dev pieter_noordhuis] NYC Taxi Trip Analysis.lvdash.json") | ||
// if err != nil { | ||
// return err | ||
// } | ||
|
||
// fmt.Println(obj.ModifiedAt) | ||
|
||
if etag != dashboard.Etag { | ||
fmt.Printf("[%s]: Updating dashboard at %s\n", dashboard.UpdateTime, relPath) | ||
d.saveSerializedDashboard(ctx, dashboard, dash.DefinitionPath) | ||
} | ||
|
||
etag = dashboard.Etag | ||
time.Sleep(1 * time.Second) | ||
} | ||
} | ||
|
||
func (d *dashboard) RunE(cmd *cobra.Command, args []string) error { | ||
ctx := cmd.Context() | ||
b, diags := root.MustConfigureBundle(cmd) | ||
if err := diags.Error(); err != nil { | ||
return diags.Error() | ||
} | ||
|
||
// Make sure we know how the dashboard path is relative to the resource path. | ||
rel, err := filepath.Rel(d.resourceDir, d.dashboardDir) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
d.relativeDir = filepath.ToSlash(rel) | ||
|
||
w := b.WorkspaceClient() | ||
|
||
if d.watch != "" { | ||
return d.watchForChanges(ctx, b) | ||
} | ||
|
||
// Lookup the dashboard ID if the path is given | ||
diags = d.resolveDashboardID(ctx, w) | ||
if diags.HasError() { | ||
return diags.Error() | ||
} | ||
|
||
dashboard, err := w.Lakeview.GetByDashboardId(ctx, d.existingDashboardId) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
d.saveConfiguration(ctx, dashboard) | ||
|
||
// TODO: add flag | ||
key := d.key | ||
if key == "" { | ||
key = textutil.NormalizeString(dashboard.DisplayName) | ||
} | ||
|
||
filename := filepath.Join(d.dashboardDir, fmt.Sprintf("%s.lvdash.json", key)) | ||
d.saveSerializedDashboard(ctx, dashboard, filename) | ||
return nil | ||
} | ||
|
||
func NewGenerateDashboardCommand() *cobra.Command { | ||
cmd := &cobra.Command{ | ||
Use: "dashboard", | ||
Short: "Generate configuration for a dashboard", | ||
} | ||
|
||
d := &dashboard{} | ||
|
||
cmd.Flags().StringVarP(&d.resourceDir, "resource-dir", "d", "./resources", `directory to write the configuration to`) | ||
cmd.Flags().StringVarP(&d.dashboardDir, "dashboard-dir", "s", "./dashboards", `directory to write the dashboard representation to`) | ||
cmd.Flags().BoolVarP(&d.force, "force", "f", false, `force overwrite existing files in the output directory`) | ||
|
||
// Specify dashboard by workspace path | ||
|
||
cmd.Flags().StringVar(&d.existingDashboardPath, "existing-dashboard-path", "", `workspace path of the dashboard to generate configuration for`) | ||
cmd.Flags().StringVar(&d.existingDashboardId, "existing-dashboard-id", "", `ID of the dashboard to generate configuration for`) | ||
cmd.Flags().StringVar(&d.watch, "watch-resource", "", `resource key of dashboard to watch for changes`) | ||
|
||
cmd.MarkFlagsOneRequired( | ||
"existing-dashboard-path", | ||
"existing-dashboard-id", | ||
"watch-resource", | ||
) | ||
|
||
cmd.RunE = d.RunE | ||
return cmd | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package generate |
Oops, something went wrong.