Skip to content

Commit 83be044

Browse files
committed
Read dashboard definition from file_path via filer interface
1 parent 8eb31cc commit 83be044

File tree

10 files changed

+175
-81
lines changed

10 files changed

+175
-81
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
bundle:
2+
name: deploy-dashboard-test-$UNIQUE_NAME
3+
4+
resources:
5+
dashboards:
6+
dashboard1:
7+
display_name: $DASHBOARD_DISPLAY_NAME
8+
warehouse_id: $TEST_DEFAULT_WAREHOUSE_ID
9+
embed_credentials: true
10+
file_path: "sample-dashboard.lvdash.json"
11+
parent_path: /Users/$CURRENT_USER_NAME
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
>>> [CLI] bundle deploy
3+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-dashboard-test-[UNIQUE_NAME]/default/files...
4+
Deploying resources...
5+
Updating deployment state...
6+
Deployment complete!
7+
8+
>>> [CLI] lakeview get [DASHBOARD_ID]
9+
{
10+
"lifecycle_state": "ACTIVE",
11+
"parent_path": "/Users/[USERNAME]",
12+
"path": "/Users/[USERNAME]/test bundle-deploy-dashboard [UUID].lvdash.json",
13+
"serialized_dashboard": "{\"pages\":[{\"name\":\"02724bf2\",\"displayName\":\"Dashboard test bundle-deploy-dashboard\"}]}"
14+
}
15+
16+
>>> [CLI] bundle destroy --auto-approve
17+
The following resources will be deleted:
18+
delete dashboard dashboard1
19+
20+
All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-dashboard-test-[UNIQUE_NAME]/default
21+
22+
Deleting files...
23+
Destroy complete!
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"pages":[{"name":"02724bf2","displayName":"Dashboard test bundle-deploy-dashboard"}]}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
DASHBOARD_DISPLAY_NAME="test bundle-deploy-dashboard $(uuid)"
2+
if [ -z "$CLOUD_ENV" ]; then
3+
DASHBOARD_DISPLAY_NAME="test bundle/deploy/ 6260d50f-e8ff-4905-8f28-812345678903" # use hard-coded uuid when running locally
4+
export TEST_DEFAULT_WAREHOUSE_ID="warehouse-1234"
5+
fi
6+
7+
export DASHBOARD_DISPLAY_NAME
8+
envsubst < databricks.yml.tmpl > databricks.yml
9+
10+
cleanup() {
11+
trace $CLI bundle destroy --auto-approve
12+
}
13+
trap cleanup EXIT
14+
15+
trace $CLI bundle deploy
16+
DASHBOARD_ID=$($CLI bundle summary --output json | jq -r '.resources.dashboards.dashboard1.id')
17+
trace $CLI lakeview get $DASHBOARD_ID | jq '{lifecycle_state, parent_path, path, serialized_dashboard}'
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
Local = true
2+
Cloud = true
3+
RequiresWarehouse = true
4+
5+
Ignore = [
6+
"databricks.yml",
7+
]
8+
9+
[[Repls]]
10+
Old = "[0-9a-f]{32}"
11+
New = "[DASHBOARD_ID]"
12+
13+
[[Server]]
14+
Pattern = "POST /api/2.0/lakeview/dashboards"
15+
Response.Body = '''
16+
{
17+
"dashboard_id":"1234567890abcdef1234567890abcdef"
18+
}
19+
'''
20+
21+
[[Server]]
22+
Pattern = "POST /api/2.0/lakeview/dashboards/{dashboard_id}/published"
23+
24+
[[Server]]
25+
Pattern = "GET /api/2.0/lakeview/dashboards/{dashboard_id}"
26+
Response.Body = '''
27+
{
28+
"dashboard_id":"1234567890abcdef1234567890abcdef",
29+
"display_name": "test dashboard 6260d50f-e8ff-4905-8f28-812345678903",
30+
"lifecycle_state": "ACTIVE",
31+
"path": "/Users/[USERNAME]/test bundle-deploy-dashboard [UUID].lvdash.json",
32+
"parent_path": "/Users/tester@databricks.com",
33+
"serialized_dashboard": "{\"pages\":[{\"name\":\"02724bf2\",\"displayName\":\"Dashboard test bundle-deploy-dashboard\"}]}"
34+
}
35+
'''
36+
37+
[[Server]]
38+
Pattern = "DELETE /api/2.0/lakeview/dashboards/{dashboard_id}"
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package resourcemutator
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/databricks/cli/bundle"
8+
"github.com/databricks/cli/libs/diag"
9+
"github.com/databricks/cli/libs/dyn"
10+
)
11+
12+
const (
13+
filePathFieldName = "file_path"
14+
serializedDashboardFieldName = "serialized_dashboard"
15+
)
16+
17+
type configureDashboardSerializedDashboard struct{}
18+
19+
func ConfigureDashboardSerializedDashboard() bundle.Mutator {
20+
return &configureDashboardSerializedDashboard{}
21+
}
22+
23+
func (c configureDashboardSerializedDashboard) Name() string {
24+
return "ConfigureDashboardSerializedDashboard"
25+
}
26+
27+
func (c configureDashboardSerializedDashboard) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
28+
var diags diag.Diagnostics
29+
30+
pattern := dyn.NewPattern(
31+
dyn.Key("resources"),
32+
dyn.Key("dashboards"),
33+
dyn.AnyKey(),
34+
)
35+
36+
// Configure serialized_dashboard field for all dashboards.
37+
err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) {
38+
return dyn.MapByPattern(v, pattern, func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
39+
// Include "serialized_dashboard" field if "file_path" is set.
40+
// Note: the Terraform resource supports "file_path" natively, but we read the contents of the dashboard here
41+
// to cover the use case of deployments from the workspace
42+
if path, ok := v.Get(filePathFieldName).AsString(); ok {
43+
44+
contents, err := b.SyncRoot.ReadFile(path)
45+
if err != nil {
46+
return dyn.InvalidValue, fmt.Errorf("failed to read serialized dashboard from file_path %s: %w", path, err)
47+
}
48+
49+
v, err := dyn.Set(v, serializedDashboardFieldName, dyn.V(string(contents)))
50+
if err != nil {
51+
return dyn.InvalidValue, fmt.Errorf("failed to set serialized_dashboard: %w", err)
52+
}
53+
54+
// Drop the "file_path" field. It is mutually exclusive with "serialized_dashboard".
55+
return dyn.Walk(v, func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
56+
switch len(p) {
57+
case 0:
58+
return v, nil
59+
case 1:
60+
if p[0] == dyn.Key(filePathFieldName) {
61+
return v, dyn.ErrDrop
62+
}
63+
}
64+
65+
// Skip everything else.
66+
return v, dyn.ErrSkip
67+
})
68+
}
69+
return v, nil
70+
})
71+
})
72+
73+
diags = diags.Extend(diag.FromErr(err))
74+
return diags
75+
}

bundle/config/mutator/resourcemutator/resource_mutator.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ func applyInitializeMutators(ctx context.Context, b *bundle.Bundle) diag.Diagnos
4141
// Updates (dynamic): resources.dashboards.*.embed_credentials (sets to false if not set)
4242
ConfigureDashboardDefaults(),
4343

44+
// Reads (typed): resources.dashboards.*.file_path (checks for existing file_path)
45+
// Updates (typed): resources.dashboards.*.serialized_dashboard (sets to the contents of the file in file_path)
46+
// Removes resources.dashboards.*.file_path
47+
ConfigureDashboardSerializedDashboard(),
48+
4449
// Reads (dynamic): resources.volumes.* (checks for existing volume_type)
4550
// Updates (dynamic): resources.volumes.*.volume_type (sets to "MANAGED" if not set)
4651
ConfigureVolumeDefaults(),

bundle/deploy/terraform/tfdyn/convert_dashboard.go

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
)
1313

1414
const (
15-
filePathFieldName = "file_path"
1615
serializedDashboardFieldName = "serialized_dashboard"
1716
)
1817

@@ -48,33 +47,6 @@ func convertDashboardResource(ctx context.Context, vin dyn.Value) (dyn.Value, er
4847
log.Debugf(ctx, "dashboard normalization diagnostic: %s", diag.Summary)
4948
}
5049

51-
// Include "serialized_dashboard" field if "file_path" is set.
52-
// Note: the Terraform resource supports "file_path" natively, but its
53-
// change detection mechanism doesn't work as expected at the time of writing (Sep 30).
54-
if path, ok := vout.Get(filePathFieldName).AsString(); ok {
55-
vout, err = dyn.Set(vout, serializedDashboardFieldName, dyn.V(fmt.Sprintf("${file(%q)}", path)))
56-
if err != nil {
57-
return dyn.InvalidValue, fmt.Errorf("failed to set serialized_dashboard: %w", err)
58-
}
59-
// Drop the "file_path" field. It is mutually exclusive with "serialized_dashboard".
60-
vout, err = dyn.Walk(vout, func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
61-
switch len(p) {
62-
case 0:
63-
return v, nil
64-
case 1:
65-
if p[0] == dyn.Key(filePathFieldName) {
66-
return v, dyn.ErrDrop
67-
}
68-
}
69-
70-
// Skip everything else.
71-
return v, dyn.ErrSkip
72-
})
73-
if err != nil {
74-
return dyn.InvalidValue, fmt.Errorf("failed to drop file_path: %w", err)
75-
}
76-
}
77-
7850
// Marshal "serialized_dashboard" as JSON if it is set in the input but not in the output.
7951
vout, err = marshalSerializedDashboard(vin, vout)
8052
if err != nil {

bundle/deploy/terraform/tfdyn/convert_dashboard_test.go

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -59,54 +59,6 @@ func TestConvertDashboard(t *testing.T) {
5959
}, out.Permissions["dashboard_my_dashboard"])
6060
}
6161

62-
func TestConvertDashboardFilePath(t *testing.T) {
63-
src := resources.Dashboard{
64-
FilePath: "some/path",
65-
}
66-
67-
vin, err := convert.FromTyped(src, dyn.NilValue)
68-
require.NoError(t, err)
69-
70-
ctx := context.Background()
71-
out := schema.NewResources()
72-
err = dashboardConverter{}.Convert(ctx, "my_dashboard", vin, out)
73-
require.NoError(t, err)
74-
75-
// Assert that the "serialized_dashboard" is included.
76-
assert.Subset(t, out.Dashboard["my_dashboard"], map[string]any{
77-
"serialized_dashboard": "${file(\"some/path\")}",
78-
})
79-
80-
// Assert that the "file_path" doesn't carry over.
81-
assert.NotSubset(t, out.Dashboard["my_dashboard"], map[string]any{
82-
"file_path": "some/path",
83-
})
84-
}
85-
86-
func TestConvertDashboardFilePathQuoted(t *testing.T) {
87-
src := resources.Dashboard{
88-
FilePath: `C:\foo\bar\baz\dashboard.lvdash.json`,
89-
}
90-
91-
vin, err := convert.FromTyped(src, dyn.NilValue)
92-
require.NoError(t, err)
93-
94-
ctx := context.Background()
95-
out := schema.NewResources()
96-
err = dashboardConverter{}.Convert(ctx, "my_dashboard", vin, out)
97-
require.NoError(t, err)
98-
99-
// Assert that the "serialized_dashboard" is included.
100-
assert.Subset(t, out.Dashboard["my_dashboard"], map[string]any{
101-
"serialized_dashboard": `${file("C:\\foo\\bar\\baz\\dashboard.lvdash.json")}`,
102-
})
103-
104-
// Assert that the "file_path" doesn't carry over.
105-
assert.NotSubset(t, out.Dashboard["my_dashboard"], map[string]any{
106-
"file_path": `C:\foo\bar\baz\dashboard.lvdash.json`,
107-
})
108-
}
109-
11062
func TestConvertDashboardSerializedDashboardString(t *testing.T) {
11163
src := resources.Dashboard{
11264
SerializedDashboard: `{ "json": true }`,

bundle/phases/initialize.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ func Initialize(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
119119
// ApplyPresets through ResourceProcessor.
120120
resourcemutator.ApplyTargetMode(),
121121

122+
// Reads (typed): b.SyncRoot (checks if bundle root is in /Workspace/)
123+
// Updates (typed): b.SyncRoot (replaces with extension-aware path when running on Databricks Runtime)
124+
// Configure use of WSFS for reads if the CLI is running on Databricks.
125+
mutator.ConfigureWSFS(),
126+
122127
// Static resources (e.g. YAML) are already loaded, we initialize and normalize them before Python
123128
resourcemutator.ProcessStaticResources(),
124129

@@ -136,11 +141,6 @@ func Initialize(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
136141
// Provides diagnostic recommendations if the current deployment identity isn't explicitly granted CAN_MANAGE permissions
137142
permissions.PermissionDiagnostics(),
138143

139-
// Reads (typed): b.SyncRoot (checks if bundle root is in /Workspace/)
140-
// Updates (typed): b.SyncRoot (replaces with extension-aware path when running on Databricks Runtime)
141-
// Configure use of WSFS for reads if the CLI is running on Databricks.
142-
mutator.ConfigureWSFS(),
143-
144144
mutator.TranslatePaths(),
145145

146146
// Reads (typed): b.Config.Experimental.PythonWheelWrapper, b.Config.Presets.SourceLinkedDeployment (checks Python wheel wrapper and deployment mode settings)

0 commit comments

Comments
 (0)