Skip to content

Translate dashboards' paths before their serialized contents are read #3006

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
### CLI

### Bundles
* Fix reading dashboard contents when the sync root is different than the bundle root ([#3006](https://github.com/databricks/cli/pull/3006))

### API Changes
1 change: 1 addition & 0 deletions acceptance/bundle/debug/out.stderr.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
10:07:59 Debug: Apply pid=12345 mutator=ProcessStaticResources
10:07:59 Debug: Apply pid=12345 mutator=ProcessStaticResources mutator=ResolveVariableReferences(resources)
10:07:59 Debug: Apply pid=12345 mutator=ProcessStaticResources mutator=NormalizePaths
10:07:59 Debug: Apply pid=12345 mutator=ProcessStaticResources mutator=TranslatePaths
10:07:59 Debug: Apply pid=12345 mutator=PythonMutator(load)
10:07:59 Debug: Apply pid=12345 mutator=PythonMutator(init)
10:07:59 Debug: Apply pid=12345 mutator=PythonMutator(load_resources)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#
# Acceptance test for deploying dashboards with the following setup:
# 1. dashboard file is inside the bundle root, but in a separate file from the .yml file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: looks like a separate directory from the directory with the .yml file.

# 2. sync root is the same as the bundle root
#
bundle:
name: deploy-dashboard-nested-folders-$UNIQUE_NAME

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#
# Acceptance test for deploying dashboards with the following setup:
# 1. dashboard file is inside the bundle root
# 2. sync root is the same as the bundle root
#
bundle:
name: deploy-dashboard-test-$UNIQUE_NAME

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#
# Acceptance test for deploying dashboards with the following setup:
# 1. dashboard file is outside the bundle root
# 2. sync root is one level above bundle root
#
bundle:
name: deploy-dashboard-outside-bundle-root-$UNIQUE_NAME
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the difference between this test and simple_syncroot? The config seems to be very similar? Can you add a comment here on in the script explaining the purpose of this test and difference from simple_syncroot?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added comments in 81972c4


sync:
paths:
- ..
include:
- ..

resources:
dashboards:
dashboard1:
display_name: $DASHBOARD_DISPLAY_NAME
warehouse_id: $TEST_DEFAULT_WAREHOUSE_ID
embed_credentials: true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(optional followup) If you are setting embed_credentials and parent_path here to match the remote API behaviour it would be better to modify our local test server to have the same defaults. I'll leave following up on this up to you though, not a blocker here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we do it in a follow-up PR. this PR does not make use of testserver yet

file_path: ../sample-dashboard.lvdash.json
parent_path: /Users/$CURRENT_USER_NAME
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-dashboard-outside-bundle-root-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> [CLI] lakeview get [DASHBOARD_ID]
{
"lifecycle_state": "ACTIVE",
"parent_path": "/Users/[USERNAME]",
"path": "/Users/[USERNAME]/test bundle-deploy-dashboard [UUID].lvdash.json",
"serialized_dashboard": "{\"pages\":[{\"name\":\"02724bf2\",\"displayName\":\"Dashboard test bundle-deploy-dashboard\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}"
}

>>> [CLI] bundle destroy --auto-approve
The following resources will be deleted:
delete dashboard dashboard1

All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-dashboard-outside-bundle-root-[UNIQUE_NAME]/default

Deleting files...
Destroy complete!
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
DASHBOARD_DISPLAY_NAME="test bundle-deploy-dashboard $(uuid)"
if [ -z "$CLOUD_ENV" ]; then
DASHBOARD_DISPLAY_NAME="test bundle/deploy/ 6260d50f-e8ff-4905-8f28-812345678903" # use hard-coded uuid when running locally
export TEST_DEFAULT_WAREHOUSE_ID="warehouse-1234"
fi
cp $TESTDIR/../simple/sample-dashboard.lvdash.json ../.

export DASHBOARD_DISPLAY_NAME
envsubst < databricks.yml.tmpl > databricks.yml

cleanup() {
trace $CLI bundle destroy --auto-approve
}
trap cleanup EXIT

trace $CLI bundle deploy
DASHBOARD_ID=$($CLI bundle summary --output json | jq -r '.resources.dashboards.dashboard1.id')
trace $CLI lakeview get $DASHBOARD_ID | jq '{lifecycle_state, parent_path, path, serialized_dashboard}'
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
Local = true
Cloud = true
RequiresWarehouse = true

Ignore = [
"databricks.yml",
"sample-dashboard.lvdash.json",
]

[[Repls]]
Old = "[0-9a-f]{32}"
New = "[DASHBOARD_ID]"

[[Repls]]
# Windows:
Old = 'The system cannot find the file specified.'
New = 'no such file or directory'

[[Server]]
Pattern = "POST /api/2.0/lakeview/dashboards"
Response.Body = '''
{
"dashboard_id":"1234567890abcdef1234567890abcdef"
}
'''

[[Server]]
Pattern = "POST /api/2.0/lakeview/dashboards/{dashboard_id}/published"

[[Server]]
Pattern = "GET /api/2.0/lakeview/dashboards/{dashboard_id}"
Response.Body = '''
{
"dashboard_id":"1234567890abcdef1234567890abcdef",
"display_name": "test dashboard 6260d50f-e8ff-4905-8f28-812345678903",
"lifecycle_state": "ACTIVE",
"path": "/Users/[USERNAME]/test bundle-deploy-dashboard 6260d50f-e8ff-4905-8f28-812345678903.lvdash.json",
"parent_path": "/Users/[email protected]",
"serialized_dashboard": "{\"pages\":[{\"name\":\"02724bf2\",\"displayName\":\"Dashboard test bundle-deploy-dashboard\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}"
}
'''

[[Server]]
Pattern = "DELETE /api/2.0/lakeview/dashboards/{dashboard_id}"
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#
# Acceptance test for deploying dashboards with the following setup:
# 1. dashboard file is within the bundle root
# 2. sync root is one level above bundle root
#
bundle:
name: deploy-dashboard-test-$UNIQUE_NAME

Expand Down
20 changes: 17 additions & 3 deletions acceptance/bundle/deploy/dashboard/simple_syncroot/output.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@

>>> [CLI] bundle deploy
Error: failed to read serialized dashboard from file_path sample-dashboard.lvdash.json: open sample-dashboard.lvdash.json: no such file or directory
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-dashboard-test-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> [CLI] lakeview get [DASHBOARD_ID]
{
"lifecycle_state": "ACTIVE",
"parent_path": "/Users/[USERNAME]",
"path": "/Users/[USERNAME]/test bundle-deploy-dashboard [UUID].lvdash.json",
"serialized_dashboard": "{\"pages\":[{\"name\":\"02724bf2\",\"displayName\":\"Dashboard test bundle-deploy-dashboard\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}"
}

>>> [CLI] bundle destroy --auto-approve
Error: failed to read serialized dashboard from file_path sample-dashboard.lvdash.json: open sample-dashboard.lvdash.json: no such file or directory
The following resources will be deleted:
delete dashboard dashboard1

Exit code: 1
All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-dashboard-test-[UNIQUE_NAME]/default

Deleting files...
Destroy complete!
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
Badness = "Cannot read dashboard, expecting deployment to succeed"
Local = true
Cloud = true
RequiresWarehouse = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ func (p processStaticResources) Apply(ctx context.Context, b *bundle.Bundle) dia
"variables",
),
mutator.NormalizePaths(),

// Translate dashboard paths into paths in the workspace file system
// This must occur after NormalizePaths but before NormalizeAndInitializeResources,
// since the latter reads dashboard files and requires fully resolved paths.
mutator.TranslatePathsDashboards(),
)
if diags.HasError() {
return diags
Expand Down
25 changes: 21 additions & 4 deletions bundle/config/mutator/translate_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,22 @@ func (err ErrIsNotNotebook) Error() string {
return fmt.Sprintf("file at %s is not a notebook", err.path)
}

type translatePaths struct{}
type translatePaths struct {
dashboardsOnly bool
}

// TranslatePaths converts paths to local notebook files into paths in the workspace file system.
func TranslatePaths() bundle.Mutator {
return &translatePaths{}
}

// TranslatePathsDashboards converts paths to local dashboard files into paths in the workspace file system.
func TranslatePathsDashboards() bundle.Mutator {
return &translatePaths{
dashboardsOnly: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that you already have a separate mutator, why not just implement Apply for it directly and keep translatePaths option-less.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that, but i do not want to copy translation logic (structs and methods) nor i want to make it public to other packages, so i decided this is the "cleanest" way to get this done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both mutators can live in the same package and in the same file. You just need to add new struct and Apply:

type translatePathsDashboards struct{}
func (m *translatePathsDashboards) Apply(...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please review #3031 to see if you like it better

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrm, I expected it to be simpler than that. We can proceed with either, up to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we merge this first, then I will rebase the refactor branch and evaluate which version i like better

}
}

func (m *translatePaths) Name() string {
return "TranslatePaths"
}
Expand Down Expand Up @@ -297,13 +306,21 @@ func (m *translatePaths) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagn

err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) {
var err error
for _, fn := range []func(context.Context, dyn.Value) (dyn.Value, error){

translations := []func(context.Context, dyn.Value) (dyn.Value, error){
t.applyJobTranslations,
t.applyPipelineTranslations,
t.applyArtifactTranslations,
t.applyDashboardTranslations,
t.applyAppsTranslations,
} {
}

if m.dashboardsOnly {
translations = []func(context.Context, dyn.Value) (dyn.Value, error){
t.applyDashboardTranslations,
}
}

for _, fn := range translations {
v, err = fn(ctx, v)
if err != nil {
return dyn.InvalidValue, err
Expand Down
Loading