From 384e2d1401ed1df7fa7b8b036ef410667e59d4c4 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 14:01:14 +0000 Subject: [PATCH 01/24] Add acceptance test for inline serialized_dashboard Prove that a dashboard whose serialized_dashboard is an inline YAML map (not a string) plans and deploys correctly, and record that the map is sent to the API as a JSON string. Both engines produce identical output. Co-authored-by: Isaac --- .../serialized-inline/databricks.yml.tmpl | 12 ++++++ .../serialized-inline/out.test.toml | 3 ++ .../dashboards/serialized-inline/output.txt | 39 +++++++++++++++++++ .../dashboards/serialized-inline/script | 19 +++++++++ .../dashboards/serialized-inline/test.toml | 8 ++++ 5 files changed, 81 insertions(+) create mode 100644 acceptance/bundle/resources/dashboards/serialized-inline/databricks.yml.tmpl create mode 100644 acceptance/bundle/resources/dashboards/serialized-inline/out.test.toml create mode 100644 acceptance/bundle/resources/dashboards/serialized-inline/output.txt create mode 100644 acceptance/bundle/resources/dashboards/serialized-inline/script create mode 100644 acceptance/bundle/resources/dashboards/serialized-inline/test.toml diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/databricks.yml.tmpl b/acceptance/bundle/resources/dashboards/serialized-inline/databricks.yml.tmpl new file mode 100644 index 00000000000..ca96f9fcf14 --- /dev/null +++ b/acceptance/bundle/resources/dashboards/serialized-inline/databricks.yml.tmpl @@ -0,0 +1,12 @@ +bundle: + name: dashboard-serialized-inline-$UNIQUE_NAME + +resources: + dashboards: + inline_dash: + display_name: Inline Dash + warehouse_id: $TEST_DEFAULT_WAREHOUSE_ID + serialized_dashboard: # inline YAML MAP = the non-string case + pages: + - name: page1 + displayName: Page 1 diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/out.test.toml b/acceptance/bundle/resources/dashboards/serialized-inline/out.test.toml new file mode 100644 index 00000000000..cef45fe553a --- /dev/null +++ b/acceptance/bundle/resources/dashboards/serialized-inline/out.test.toml @@ -0,0 +1,3 @@ +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/output.txt b/acceptance/bundle/resources/dashboards/serialized-inline/output.txt new file mode 100644 index 00000000000..389e62fcdeb --- /dev/null +++ b/acceptance/bundle/resources/dashboards/serialized-inline/output.txt @@ -0,0 +1,39 @@ + +>>> [CLI] bundle plan +create dashboards.inline_dash + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dashboard-serialized-inline-[UNIQUE_NAME]/default/files... +Created dashboards.inline_dash +Files: 6 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +>>> print_requests.py //api/2.0/lakeview/dashboards +{ + "method": "POST", + "path": "/api/2.0/lakeview/dashboards", + "body": { + "display_name": "Inline Dash", + "parent_path": "/Workspace/Users/[USERNAME]/.bundle/dashboard-serialized-inline-[UNIQUE_NAME]/default/resources", + "serialized_dashboard": "{\"pages\":[{\"displayName\":\"Page 1\",\"name\":\"page1\"}]}", + "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" + } +} +{ + "method": "POST", + "path": "/api/2.0/lakeview/dashboards/[DASHBOARD_ID]/published", + "body": { + "embed_credentials": false, + "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" + } +} + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.dashboards.inline_dash + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/dashboard-serialized-inline-[UNIQUE_NAME]/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/script b/acceptance/bundle/resources/dashboards/serialized-inline/script new file mode 100644 index 00000000000..04ca23512e0 --- /dev/null +++ b/acceptance/bundle/resources/dashboards/serialized-inline/script @@ -0,0 +1,19 @@ +envsubst < databricks.yml.tmpl > databricks.yml + +cleanup() { + trace $CLI bundle destroy --auto-approve + rm out.requests.txt +} +trap cleanup EXIT + +trace $CLI bundle plan +trace $CLI bundle deploy + +# Normalize the non-deterministic dashboard ID so the published-dashboard request +# path is stable across engines and runs. +dashboard_id=$($CLI bundle summary --output json | jq -r '.resources.dashboards.inline_dash.id') +add_repl "$dashboard_id" DASHBOARD_ID + +# serialized_dashboard is authored as an inline YAML map but must reach the API as a +# JSON *string*, not a nested object. Record the create request to prove that. +trace print_requests.py //api/2.0/lakeview/dashboards diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/test.toml b/acceptance/bundle/resources/dashboards/serialized-inline/test.toml new file mode 100644 index 00000000000..a5edf8715ad --- /dev/null +++ b/acceptance/bundle/resources/dashboards/serialized-inline/test.toml @@ -0,0 +1,8 @@ +Ignore = [ + "databricks.yml", +] + +# This test does no read-after-deploy, so the inherited eventual-consistency stale GET +# is unnecessary. With it enabled, the direct-engine destroy's first GET 404s and reports +# "0 deleted", diverging from terraform. Disable it so both engines destroy identically. +Env.INJECT_STALE_ON_DIRECT = "0" From b806ba114728daf8454c296c31e6ee3ddbe14639 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 14:10:42 +0000 Subject: [PATCH 02/24] Add acceptance test for changing an inline serialized_dashboard Deploy a dashboard whose serialized_dashboard is an inline YAML map, then change a value inside the map and show the plan: the change is detected as an update (not a recreate). Capture the update request the direct engine sends, which serializes the inline map to a JSON string. Co-authored-by: Isaac --- .../databricks.yml.tmpl | 12 ++++++ .../out.requests.direct.txt | 18 +++++++++ .../change-serialized-inline/out.test.toml | 3 ++ .../change-serialized-inline/output.txt | 25 ++++++++++++ .../change-serialized-inline/script | 38 +++++++++++++++++++ .../change-serialized-inline/test.toml | 3 ++ 6 files changed, 99 insertions(+) create mode 100644 acceptance/bundle/resources/dashboards/change-serialized-inline/databricks.yml.tmpl create mode 100644 acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.direct.txt create mode 100644 acceptance/bundle/resources/dashboards/change-serialized-inline/out.test.toml create mode 100644 acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt create mode 100644 acceptance/bundle/resources/dashboards/change-serialized-inline/script create mode 100644 acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/databricks.yml.tmpl b/acceptance/bundle/resources/dashboards/change-serialized-inline/databricks.yml.tmpl new file mode 100644 index 00000000000..c5f5494f367 --- /dev/null +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/databricks.yml.tmpl @@ -0,0 +1,12 @@ +bundle: + name: change-serialized-inline-$UNIQUE_NAME + +resources: + dashboards: + inline_dash: + display_name: Inline Dash + warehouse_id: $TEST_DEFAULT_WAREHOUSE_ID + serialized_dashboard: # inline YAML MAP = the non-string case + pages: + - name: page1 + displayName: $PAGE_DISPLAY_NAME diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.direct.txt b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.direct.txt new file mode 100644 index 00000000000..763f6fc22c7 --- /dev/null +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.direct.txt @@ -0,0 +1,18 @@ +{ + "method": "PATCH", + "path": "/api/2.0/lakeview/dashboards/[DASHBOARD_ID]", + "body": { + "display_name": "Inline Dash", + "parent_path": "/Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources", + "serialized_dashboard": "{\"pages\":[{\"displayName\":\"Page One\",\"name\":\"page1\"}]}", + "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" + } +} +{ + "method": "POST", + "path": "/api/2.0/lakeview/dashboards/[DASHBOARD_ID]/published", + "body": { + "embed_credentials": false, + "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" + } +} diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.test.toml b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.test.toml new file mode 100644 index 00000000000..cef45fe553a --- /dev/null +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.test.toml @@ -0,0 +1,3 @@ +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt b/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt new file mode 100644 index 00000000000..e41d7092757 --- /dev/null +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt @@ -0,0 +1,25 @@ + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/files... +Created dashboards.inline_dash +Files: 6 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +>>> [CLI] bundle plan +update dashboards.inline_dash + +Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/files... +Updated dashboards.inline_dash +Files: 3 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.dashboards.inline_dash + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/script b/acceptance/bundle/resources/dashboards/change-serialized-inline/script new file mode 100644 index 00000000000..8f10ff62903 --- /dev/null +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/script @@ -0,0 +1,38 @@ +cleanup() { + trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt +} +trap cleanup EXIT + +# Deploy a dashboard whose serialized_dashboard is an inline YAML map. +export PAGE_DISPLAY_NAME="Page 1" +envsubst < databricks.yml.tmpl > databricks.yml +trace $CLI bundle deploy + +dashboard_id=$($CLI bundle summary --output json | jq -r '.resources.dashboards.inline_dash.id') +add_repl "$dashboard_id" DASHBOARD_ID + +# Wait out eventual consistency so the change below is planned against the deployed +# state (the first read of a freshly created dashboard 404s on the direct engine). +retry --until "Page 1" $CLI lakeview get "$dashboard_id" > /dev/null + +# Change a value *inside* the inline serialized_dashboard, then show the plan. The +# change must be detected as an update to the existing dashboard, not a recreate. +export PAGE_DISPLAY_NAME="Page One" +envsubst < databricks.yml.tmpl > databricks.yml +trace $CLI bundle plan + +# Discard the requests from the initial deploy so the recording below shows only what +# deploying the change sends. Direct only: terraform echoes OUTPUT_ONLY fields in the +# request body, and the direct engine is the default we care about here. +if [ "$DATABRICKS_BUNDLE_ENGINE" = "direct" ]; then + print_requests.py //api/2.0/lakeview/dashboards > /dev/null +fi + +# Deploy the change. The update serializes the inline YAML map to a JSON string, same +# as create. +trace $CLI bundle deploy + +if [ "$DATABRICKS_BUNDLE_ENGINE" = "direct" ]; then + print_requests.py //api/2.0/lakeview/dashboards > out.requests.direct.txt +fi diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml b/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml new file mode 100644 index 00000000000..17383eac468 --- /dev/null +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml @@ -0,0 +1,3 @@ +Ignore = [ + "databricks.yml", +] From aba995ffcadf41ffd559fff2814ab144263965b4 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 14:17:11 +0000 Subject: [PATCH 03/24] Output the JSON plan and drop request capture in change test Show the machine-readable plan (per engine) for the serialized_dashboard change instead of recording the update request. Co-authored-by: Isaac --- .../out.plan.direct.json | 95 +++++++++++++++++++ .../out.plan.terraform.json | 8 ++ .../out.requests.direct.txt | 18 ---- .../change-serialized-inline/output.txt | 8 +- .../change-serialized-inline/script | 18 +--- .../change-serialized-inline/test.toml | 1 + 6 files changed, 109 insertions(+), 39 deletions(-) create mode 100644 acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json create mode 100644 acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.terraform.json delete mode 100644 acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.direct.txt diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json new file mode 100644 index 00000000000..b9244b3ab9d --- /dev/null +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json @@ -0,0 +1,95 @@ +{ + "plan_version": 2, + "cli_version": "[CLI_VERSION]", + "lineage": "[UUID]", + "serial": 1, + "plan": { + "resources.dashboards.inline_dash": { + "action": "update", + "new_state": { + "value": { + "display_name": "Inline Dash", + "embed_credentials": false, + "parent_path": "/Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources", + "published": true, + "serialized_dashboard": { + "pages": [ + { + "displayName": "Page One", + "name": "page1" + } + ] + }, + "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" + } + }, + "remote_state": { + "create_time": "[TIMESTAMP]", + "dashboard_id": "[DASHBOARD_ID]", + "display_name": "Inline Dash", + "embed_credentials": false, + "etag": "[NUMID]", + "lifecycle_state": "ACTIVE", + "parent_path": "/Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources", + "path": "/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources/Inline Dash.lvdash.json", + "published": true, + "serialized_dashboard": "{\"pages\":[{\"displayName\":\"Page 1\",\"name\":\"page1\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}\n", + "update_time": "[TIMESTAMP]", + "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" + }, + "changes": { + "create_time": { + "action": "skip", + "reason": "spec:output_only", + "remote": "[TIMESTAMP]" + }, + "dashboard_id": { + "action": "skip", + "reason": "spec:output_only", + "remote": "[DASHBOARD_ID]" + }, + "etag": { + "action": "skip", + "reason": "custom", + "old": "[NUMID]", + "remote": "[NUMID]" + }, + "lifecycle_state": { + "action": "skip", + "reason": "spec:output_only", + "remote": "ACTIVE" + }, + "path": { + "action": "skip", + "reason": "spec:output_only", + "remote": "/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources/Inline Dash.lvdash.json" + }, + "serialized_dashboard": { + "action": "update", + "old": { + "pages": [ + { + "displayName": "Page 1", + "name": "page1" + } + ] + }, + "new": { + "pages": [ + { + "displayName": "Page One", + "name": "page1" + } + ] + }, + "remote": "{\"pages\":[{\"displayName\":\"Page 1\",\"name\":\"page1\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}\n" + }, + "update_time": { + "action": "skip", + "reason": "spec:output_only", + "remote": "[TIMESTAMP]" + } + } + } + } +} diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.terraform.json b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.terraform.json new file mode 100644 index 00000000000..7795272689a --- /dev/null +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.terraform.json @@ -0,0 +1,8 @@ +{ + "cli_version": "[CLI_VERSION]", + "plan": { + "resources.dashboards.inline_dash": { + "action": "update" + } + } +} diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.direct.txt b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.direct.txt deleted file mode 100644 index 763f6fc22c7..00000000000 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.direct.txt +++ /dev/null @@ -1,18 +0,0 @@ -{ - "method": "PATCH", - "path": "/api/2.0/lakeview/dashboards/[DASHBOARD_ID]", - "body": { - "display_name": "Inline Dash", - "parent_path": "/Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources", - "serialized_dashboard": "{\"pages\":[{\"displayName\":\"Page One\",\"name\":\"page1\"}]}", - "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" - } -} -{ - "method": "POST", - "path": "/api/2.0/lakeview/dashboards/[DASHBOARD_ID]/published", - "body": { - "embed_credentials": false, - "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" - } -} diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt b/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt index e41d7092757..c675206603d 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt @@ -2,7 +2,7 @@ >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/files... Created dashboards.inline_dash -Files: 6 uploaded, 0 deleted +Files: 5 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged >>> [CLI] bundle plan @@ -10,11 +10,7 @@ update dashboards.inline_dash Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged ->>> [CLI] bundle deploy -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/files... -Updated dashboards.inline_dash -Files: 3 uploaded, 0 deleted -Resources: 0 created, 1 changed, 0 deleted, 0 unchanged +>>> [CLI] bundle plan -o json >>> [CLI] bundle destroy --auto-approve The following resources will be deleted: diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/script b/acceptance/bundle/resources/dashboards/change-serialized-inline/script index 8f10ff62903..eb5592ec881 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/script @@ -1,6 +1,5 @@ cleanup() { trace $CLI bundle destroy --auto-approve - rm -f out.requests.txt } trap cleanup EXIT @@ -22,17 +21,6 @@ export PAGE_DISPLAY_NAME="Page One" envsubst < databricks.yml.tmpl > databricks.yml trace $CLI bundle plan -# Discard the requests from the initial deploy so the recording below shows only what -# deploying the change sends. Direct only: terraform echoes OUTPUT_ONLY fields in the -# request body, and the direct engine is the default we care about here. -if [ "$DATABRICKS_BUNDLE_ENGINE" = "direct" ]; then - print_requests.py //api/2.0/lakeview/dashboards > /dev/null -fi - -# Deploy the change. The update serializes the inline YAML map to a JSON string, same -# as create. -trace $CLI bundle deploy - -if [ "$DATABRICKS_BUNDLE_ENGINE" = "direct" ]; then - print_requests.py //api/2.0/lakeview/dashboards > out.requests.direct.txt -fi +# Capture the machine-readable plan too. The plan structure differs between engines, +# so each engine writes its own file. +trace $CLI bundle plan -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml b/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml index 17383eac468..3c873441e45 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml @@ -1,3 +1,4 @@ Ignore = [ "databricks.yml", ] +RecordRequests = false From 0e491079a6e3236d32906d1419d7acc015444843 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 18:34:02 +0000 Subject: [PATCH 04/24] Stop overriding INJECT_STALE_ON_DIRECT in serialized-inline test The direct-engine destroy 404s on the injected stale GET and reports "0 deleted" while terraform reports "1 deleted". That divergence is only in the cleanup step, so route destroy output to a LOG file instead of disabling the inherited stale-GET simulation. Co-authored-by: Isaac --- .../resources/dashboards/serialized-inline/output.txt | 8 -------- .../bundle/resources/dashboards/serialized-inline/script | 7 +++++-- .../resources/dashboards/serialized-inline/test.toml | 5 ----- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/output.txt b/acceptance/bundle/resources/dashboards/serialized-inline/output.txt index 389e62fcdeb..5fbda9f2d9e 100644 --- a/acceptance/bundle/resources/dashboards/serialized-inline/output.txt +++ b/acceptance/bundle/resources/dashboards/serialized-inline/output.txt @@ -29,11 +29,3 @@ Resources: 1 created, 0 changed, 0 deleted, 0 unchanged "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" } } - ->>> [CLI] bundle destroy --auto-approve -The following resources will be deleted: - delete resources.dashboards.inline_dash - -All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/dashboard-serialized-inline-[UNIQUE_NAME]/default - -Destroy: 1 deleted diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/script b/acceptance/bundle/resources/dashboards/serialized-inline/script index 04ca23512e0..befe8c09248 100644 --- a/acceptance/bundle/resources/dashboards/serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/serialized-inline/script @@ -1,8 +1,11 @@ envsubst < databricks.yml.tmpl > databricks.yml cleanup() { - trace $CLI bundle destroy --auto-approve - rm out.requests.txt + # Cleanup only. The direct engine's destroy 404s on the injected stale GET and + # reports "0 deleted" while terraform reports "1 deleted", so keep it out of the + # compared output. + $CLI bundle destroy --auto-approve &> LOG.destroy + rm -f out.requests.txt } trap cleanup EXIT diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/test.toml b/acceptance/bundle/resources/dashboards/serialized-inline/test.toml index a5edf8715ad..17383eac468 100644 --- a/acceptance/bundle/resources/dashboards/serialized-inline/test.toml +++ b/acceptance/bundle/resources/dashboards/serialized-inline/test.toml @@ -1,8 +1,3 @@ Ignore = [ "databricks.yml", ] - -# This test does no read-after-deploy, so the inherited eventual-consistency stale GET -# is unnecessary. With it enabled, the direct-engine destroy's first GET 404s and reports -# "0 deleted", diverging from terraform. Disable it so both engines destroy identically. -Env.INJECT_STALE_ON_DIRECT = "0" From 25cd3863626a5c05830aaf69e4ccb5069e8f3c92 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 18:46:36 +0000 Subject: [PATCH 05/24] Address review: update_file.py, READPLAN, and capture the update request - Change the inline serialized_dashboard value with update_file.py, which asserts if the target text is missing instead of silently no-op-ing. - Deploy the change and test the READPLAN matrix (in-memory vs saved plan); both send the same update request. - Capture the direct-engine update request alongside the per-engine JSON plans. Co-authored-by: Isaac --- .../databricks.yml.tmpl | 2 +- .../out.requests.direct.txt | 18 ++++++++++++ .../change-serialized-inline/out.test.toml | 1 + .../change-serialized-inline/output.txt | 8 ++++- .../change-serialized-inline/script | 29 ++++++++++++++----- .../change-serialized-inline/test.toml | 3 +- 6 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.direct.txt diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/databricks.yml.tmpl b/acceptance/bundle/resources/dashboards/change-serialized-inline/databricks.yml.tmpl index c5f5494f367..0942aa4f690 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/databricks.yml.tmpl +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/databricks.yml.tmpl @@ -9,4 +9,4 @@ resources: serialized_dashboard: # inline YAML MAP = the non-string case pages: - name: page1 - displayName: $PAGE_DISPLAY_NAME + displayName: Page 1 diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.direct.txt b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.direct.txt new file mode 100644 index 00000000000..763f6fc22c7 --- /dev/null +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.direct.txt @@ -0,0 +1,18 @@ +{ + "method": "PATCH", + "path": "/api/2.0/lakeview/dashboards/[DASHBOARD_ID]", + "body": { + "display_name": "Inline Dash", + "parent_path": "/Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources", + "serialized_dashboard": "{\"pages\":[{\"displayName\":\"Page One\",\"name\":\"page1\"}]}", + "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" + } +} +{ + "method": "POST", + "path": "/api/2.0/lakeview/dashboards/[DASHBOARD_ID]/published", + "body": { + "embed_credentials": false, + "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" + } +} diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.test.toml b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.test.toml index cef45fe553a..b1bef937a27 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.test.toml +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.test.toml @@ -1,3 +1,4 @@ Cloud = true EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] EnvMatrix.DMS = ["", "true"] +EnvMatrix.READPLAN = ["", "1"] diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt b/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt index c675206603d..ca5a2744eaf 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt @@ -2,7 +2,7 @@ >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/files... Created dashboards.inline_dash -Files: 5 uploaded, 0 deleted +Files: 6 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged >>> [CLI] bundle plan @@ -12,6 +12,12 @@ Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged >>> [CLI] bundle plan -o json +=== deploy the change +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/files... +Updated dashboards.inline_dash +Files: 4 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged + >>> [CLI] bundle destroy --auto-approve The following resources will be deleted: delete resources.dashboards.inline_dash diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/script b/acceptance/bundle/resources/dashboards/change-serialized-inline/script index eb5592ec881..b6d5bff961b 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/script @@ -1,10 +1,10 @@ cleanup() { trace $CLI bundle destroy --auto-approve + rm -f out.requests.txt } trap cleanup EXIT # Deploy a dashboard whose serialized_dashboard is an inline YAML map. -export PAGE_DISPLAY_NAME="Page 1" envsubst < databricks.yml.tmpl > databricks.yml trace $CLI bundle deploy @@ -15,12 +15,27 @@ add_repl "$dashboard_id" DASHBOARD_ID # state (the first read of a freshly created dashboard 404s on the direct engine). retry --until "Page 1" $CLI lakeview get "$dashboard_id" > /dev/null -# Change a value *inside* the inline serialized_dashboard, then show the plan. The -# change must be detected as an update to the existing dashboard, not a recreate. -export PAGE_DISPLAY_NAME="Page One" -envsubst < databricks.yml.tmpl > databricks.yml +# Change a value *inside* the inline serialized_dashboard. update_file.py asserts if the +# text is missing, so a broken test fails loudly instead of silently doing nothing. +update_file.py databricks.yml "Page 1" "Page One" + +# The change must be detected as an update to the existing dashboard, not a recreate. trace $CLI bundle plan -# Capture the machine-readable plan too. The plan structure differs between engines, -# so each engine writes its own file. +# Machine-readable plan (per engine); also fed to the READPLAN=1 deploy below. trace $CLI bundle plan -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json + +# Discard the requests from the initial deploy so the capture below shows only what +# deploying the change sends (direct only; terraform echoes OUTPUT_ONLY fields). +if [ "$DATABRICKS_BUNDLE_ENGINE" = "direct" ]; then + print_requests.py //api/2.0/lakeview/dashboards > /dev/null +fi + +# Deploy the change, in-memory (READPLAN="") or from the saved plan (READPLAN="1"). A +# saved-plan deploy must send the same request as an in-memory one. +title "deploy the change\n" +$CLI bundle deploy $(readplanarg out.plan.$DATABRICKS_BUNDLE_ENGINE.json) + +if [ "$DATABRICKS_BUNDLE_ENGINE" = "direct" ]; then + print_requests.py //api/2.0/lakeview/dashboards > out.requests.direct.txt +fi diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml b/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml index 3c873441e45..eb67230b265 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml @@ -1,4 +1,5 @@ Ignore = [ "databricks.yml", ] -RecordRequests = false + +EnvMatrix.READPLAN = ["", "1"] From 2cd393434417e00afa0bc372f3ac3a9950af0bcd Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 18:48:19 +0000 Subject: [PATCH 06/24] Trim verbose comments in the serialized-inline test scripts Co-authored-by: Isaac --- .../dashboards/change-serialized-inline/script | 18 +++++++----------- .../dashboards/serialized-inline/script | 10 +++------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/script b/acceptance/bundle/resources/dashboards/change-serialized-inline/script index b6d5bff961b..2ad282edd7c 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/script @@ -4,35 +4,31 @@ cleanup() { } trap cleanup EXIT -# Deploy a dashboard whose serialized_dashboard is an inline YAML map. +# Deploy an inline-YAML-map dashboard. envsubst < databricks.yml.tmpl > databricks.yml trace $CLI bundle deploy dashboard_id=$($CLI bundle summary --output json | jq -r '.resources.dashboards.inline_dash.id') add_repl "$dashboard_id" DASHBOARD_ID -# Wait out eventual consistency so the change below is planned against the deployed -# state (the first read of a freshly created dashboard 404s on the direct engine). +# Wait out eventual consistency (direct's first read 404s) before changing. retry --until "Page 1" $CLI lakeview get "$dashboard_id" > /dev/null -# Change a value *inside* the inline serialized_dashboard. update_file.py asserts if the -# text is missing, so a broken test fails loudly instead of silently doing nothing. +# Change a value inside the map; update_file.py asserts if the text is missing. update_file.py databricks.yml "Page 1" "Page One" -# The change must be detected as an update to the existing dashboard, not a recreate. +# Should be detected as an update, not a recreate. trace $CLI bundle plan -# Machine-readable plan (per engine); also fed to the READPLAN=1 deploy below. +# Per-engine JSON plan; also the saved plan for READPLAN=1. trace $CLI bundle plan -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json -# Discard the requests from the initial deploy so the capture below shows only what -# deploying the change sends (direct only; terraform echoes OUTPUT_ONLY fields). +# Drop initial-deploy requests so we capture only the change (direct only). if [ "$DATABRICKS_BUNDLE_ENGINE" = "direct" ]; then print_requests.py //api/2.0/lakeview/dashboards > /dev/null fi -# Deploy the change, in-memory (READPLAN="") or from the saved plan (READPLAN="1"). A -# saved-plan deploy must send the same request as an in-memory one. +# Deploy the change: in-memory (READPLAN="") or from the saved plan (READPLAN="1"). title "deploy the change\n" $CLI bundle deploy $(readplanarg out.plan.$DATABRICKS_BUNDLE_ENGINE.json) diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/script b/acceptance/bundle/resources/dashboards/serialized-inline/script index befe8c09248..1cbe3b8b0df 100644 --- a/acceptance/bundle/resources/dashboards/serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/serialized-inline/script @@ -1,9 +1,7 @@ envsubst < databricks.yml.tmpl > databricks.yml cleanup() { - # Cleanup only. The direct engine's destroy 404s on the injected stale GET and - # reports "0 deleted" while terraform reports "1 deleted", so keep it out of the - # compared output. + # Cleanup; direct's destroy 404s on the stale GET and diverges, so keep it out of the diff. $CLI bundle destroy --auto-approve &> LOG.destroy rm -f out.requests.txt } @@ -12,11 +10,9 @@ trap cleanup EXIT trace $CLI bundle plan trace $CLI bundle deploy -# Normalize the non-deterministic dashboard ID so the published-dashboard request -# path is stable across engines and runs. +# Stabilize the non-deterministic dashboard ID in recorded requests. dashboard_id=$($CLI bundle summary --output json | jq -r '.resources.dashboards.inline_dash.id') add_repl "$dashboard_id" DASHBOARD_ID -# serialized_dashboard is authored as an inline YAML map but must reach the API as a -# JSON *string*, not a nested object. Record the create request to prove that. +# Prove the inline YAML map reaches the API as a JSON string. trace print_requests.py //api/2.0/lakeview/dashboards From 484300e1d3b28b76bfcacc1984cea9e076089a57 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 18:53:22 +0000 Subject: [PATCH 07/24] Serialize inline serialized_dashboard to a JSON string early Marshal an inline (structured) serialized_dashboard to a JSON string in phases.Initialize, mirroring ConfigureGenieSpaceSerializedSpace, so the value is always a plain string downstream. A map left in config vs a JSON round-trip in state decode ints differently (Go int vs float64), which made the direct engine report false serialized_dashboard drift on every plan; the remote also comes back serialized, so a deep diff can't converge. - ConfigureDashboardSerializedDashboard now rejects setting both file_path and serialized_dashboard, and marshals an inline map/sequence to a JSON string (string/nil pass through, other kinds error). Keeps the file_path read. - prepareDashboardRequest and the tfdyn converter now assume a string; the map-marshal branches are removed. Co-authored-by: Isaac --- .../out.plan.direct.json | 27 +--- ...nfigure_dashboards_serialized_dashboard.go | 57 +++++++-- ...re_dashboards_serialized_dashboard_test.go | 120 ++++++++++++++++++ .../terraform/tfdyn/convert_dashboard.go | 38 +----- .../terraform/tfdyn/convert_dashboard_test.go | 15 +-- bundle/direct/dresources/all_test.go | 16 +-- bundle/direct/dresources/dashboard.go | 21 ++- 7 files changed, 190 insertions(+), 104 deletions(-) create mode 100644 bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard_test.go diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json index b9244b3ab9d..e14ec2fba2a 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json @@ -12,14 +12,7 @@ "embed_credentials": false, "parent_path": "/Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources", "published": true, - "serialized_dashboard": { - "pages": [ - { - "displayName": "Page One", - "name": "page1" - } - ] - }, + "serialized_dashboard": "{\"pages\":[{\"displayName\":\"Page One\",\"name\":\"page1\"}]}", "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" } }, @@ -66,22 +59,8 @@ }, "serialized_dashboard": { "action": "update", - "old": { - "pages": [ - { - "displayName": "Page 1", - "name": "page1" - } - ] - }, - "new": { - "pages": [ - { - "displayName": "Page One", - "name": "page1" - } - ] - }, + "old": "{\"pages\":[{\"displayName\":\"Page 1\",\"name\":\"page1\"}]}", + "new": "{\"pages\":[{\"displayName\":\"Page One\",\"name\":\"page1\"}]}", "remote": "{\"pages\":[{\"displayName\":\"Page 1\",\"name\":\"page1\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}\n" }, "update_time": { diff --git a/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go index 27b55403d88..4cab2c9ff9c 100644 --- a/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go +++ b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go @@ -2,6 +2,7 @@ package resourcemutator import ( "context" + "encoding/json" "fmt" "github.com/databricks/cli/bundle" @@ -36,20 +37,54 @@ func (c configureDashboardSerializedDashboard) Apply(_ context.Context, b *bundl // Configure serialized_dashboard field for all dashboards. err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) { return dyn.MapByPattern(v, pattern, func(p dyn.Path, v dyn.Value) (dyn.Value, error) { - // Include "serialized_dashboard" field if "file_path" is set. - // Note: the Terraform resource supports "file_path" natively, but we read the contents of the dashboard here - // to be able to read file contents in Databricks Workspace (reading a dashboard file via file system fails there) - path, ok := v.Get(filePathFieldName).AsString() - if !ok { - return v, nil - } + filePath, hasFilePath := v.Get(filePathFieldName).AsString() + sd := v.Get(serializedDashboardFieldName) - contents, err := b.SyncRoot.ReadFile(path) - if err != nil { - return dyn.InvalidValue, fmt.Errorf("failed to read serialized dashboard from file_path %s: %w", path, err) + if hasFilePath { + // file_path and serialized_dashboard are two ways to provide the + // same content. Accepting both is ambiguous, so reject it instead + // of silently picking one. + if sd.IsValid() && sd.Kind() != dyn.KindNil { + diags = diags.Append(diag.Diagnostic{ + Severity: diag.Error, + Summary: "both file_path and serialized_dashboard are set; specify only one", + Locations: sd.Locations(), + }) + return v, nil + } + // Note: the Terraform resource supports "file_path" natively, but we read the contents of the dashboard here + // to be able to read file contents in Databricks Workspace (reading a dashboard file via file system fails there) + contents, err := b.SyncRoot.ReadFile(filePath) + if err != nil { + return dyn.InvalidValue, fmt.Errorf("failed to read serialized dashboard from file_path %s: %w", filePath, err) + } + return dyn.Set(v, serializedDashboardFieldName, dyn.V(string(contents))) } - return dyn.Set(v, serializedDashboardFieldName, dyn.V(string(contents))) + // Marshal an inline structured serialized_dashboard to a JSON string so + // both config-side and state-side carry the same plain string. + // Otherwise YAML decodes small ints as Go `int` while state JSON + // round-trip decodes them as `float64`, and structdiff reports + // false drift on every plan. + switch sd.Kind() { + case dyn.KindInvalid, dyn.KindNil, dyn.KindString: + // KindInvalid means serialized_dashboard is absent (neither it nor + // file_path is set); leave it for backend validation to reject. + return v, nil + case dyn.KindMap, dyn.KindSequence: + jsonBytes, err := json.Marshal(sd.AsAny()) + if err != nil { + return dyn.InvalidValue, fmt.Errorf("failed to marshal inline serialized_dashboard: %w", err) + } + return dyn.Set(v, serializedDashboardFieldName, dyn.V(string(jsonBytes))) + default: + diags = diags.Append(diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("serialized_dashboard must be a string, map, or sequence, got %s", sd.Kind()), + Locations: sd.Locations(), + }) + return v, nil + } }) }) diff --git a/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard_test.go b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard_test.go new file mode 100644 index 00000000000..7800e4741a5 --- /dev/null +++ b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard_test.go @@ -0,0 +1,120 @@ +package resourcemutator_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/mutator/resourcemutator" + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/libs/vfs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfigureDashboardSerializedDashboard(t *testing.T) { + const fileName = "dashboard.lvdash.json" + + tests := []struct { + name string + // filePath is set on the resource as-is (already sync-root-relative). + filePath string + // writeFile creates filePath with fileContents before the mutator runs. + writeFile bool + fileContents string + setSerialized bool + serializedDashboard any + // wantSerialized is the expected serialized_dashboard after a successful run. + wantSerialized any + // wantErr, when non-empty, is a substring expected in the diagnostics. + wantErr string + }{ + { + // The file is read verbatim, so formatting and the trailing newline + // are preserved (unlike the inline path, which re-marshals). + name: "file_path reads file contents verbatim", + filePath: fileName, + writeFile: true, + fileContents: `{"pages": 1}` + "\n", + wantSerialized: `{"pages": 1}` + "\n", + }, + { + // Inline maps are marshaled to a compact JSON string with sorted keys + // so config and state hold an identical string and don't drift. + name: "inline map is marshaled to a JSON string", + setSerialized: true, + serializedDashboard: map[string]any{"pages": 1}, + wantSerialized: `{"pages":1}`, + }, + { + name: "inline string is left unchanged", + setSerialized: true, + serializedDashboard: `{"pages":1}`, + wantSerialized: `{"pages":1}`, + }, + { + // Neither field set: the absent field must pass through, not error. + name: "neither file_path nor serialized_dashboard passes through", + wantSerialized: nil, + }, + { + name: "both file_path and serialized_dashboard is rejected", + filePath: fileName, + setSerialized: true, + serializedDashboard: map[string]any{"pages": 1}, + wantErr: "both file_path and serialized_dashboard are set; specify only one", + }, + { + name: "non-structured serialized_dashboard is rejected", + setSerialized: true, + serializedDashboard: true, + wantErr: "serialized_dashboard must be a string, map, or sequence, got bool", + }, + { + name: "unreadable file_path is an error", + filePath: "does_not_exist.json", + wantErr: "failed to read serialized dashboard", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + if tt.writeFile { + require.NoError(t, os.WriteFile(filepath.Join(dir, tt.filePath), []byte(tt.fileContents), 0o600)) + } + + dash := &resources.Dashboard{ + DashboardConfig: resources.DashboardConfig{DisplayName: "My Dashboard"}, + FilePath: tt.filePath, + } + if tt.setSerialized { + dash.SerializedDashboard = tt.serializedDashboard + } + + b := &bundle.Bundle{ + SyncRootPath: dir, + BundleRootPath: dir, + SyncRoot: vfs.MustNew(dir), + Config: config.Root{ + Resources: config.Resources{ + Dashboards: map[string]*resources.Dashboard{"my_dashboard": dash}, + }, + }, + } + + diags := bundle.ApplySeq(t.Context(), b, resourcemutator.ConfigureDashboardSerializedDashboard()) + + if tt.wantErr != "" { + require.Error(t, diags.Error()) + assert.ErrorContains(t, diags.Error(), tt.wantErr) + return + } + + require.NoError(t, diags.Error()) + assert.Equal(t, tt.wantSerialized, b.Config.Resources.Dashboards["my_dashboard"].SerializedDashboard) + }) + } +} diff --git a/bundle/deploy/terraform/tfdyn/convert_dashboard.go b/bundle/deploy/terraform/tfdyn/convert_dashboard.go index eb9260e8ec8..e55cb6b83de 100644 --- a/bundle/deploy/terraform/tfdyn/convert_dashboard.go +++ b/bundle/deploy/terraform/tfdyn/convert_dashboard.go @@ -2,7 +2,6 @@ package tfdyn import ( "context" - "encoding/json" "fmt" "github.com/databricks/cli/bundle/internal/tf/schema" @@ -11,48 +10,17 @@ import ( "github.com/databricks/cli/libs/log" ) -const ( - serializedDashboardFieldName = "serialized_dashboard" -) - -// Marshal "serialized_dashboard" as JSON if it is set in the input but not in the output. -func marshalSerializedDashboard(vin, vout dyn.Value) (dyn.Value, error) { - // Skip if the "serialized_dashboard" field is already set. - if v := vout.Get(serializedDashboardFieldName); v.IsValid() { - return vout, nil - } - - // Skip if the "serialized_dashboard" field on the input is not set. - v := vin.Get(serializedDashboardFieldName) - if !v.IsValid() { - return vout, nil - } - - // Marshal the "serialized_dashboard" field as JSON. - data, err := json.Marshal(v.AsAny()) - if err != nil { - return dyn.InvalidValue, fmt.Errorf("failed to marshal serialized_dashboard: %w", err) - } - - // Set the "serialized_dashboard" field on the output. - return dyn.Set(vout, serializedDashboardFieldName, dyn.V(string(data))) -} - func convertDashboardResource(ctx context.Context, vin dyn.Value) (dyn.Value, error) { var err error - // Normalize the output value to the target schema. + // Normalize the output value to the target schema. ConfigureDashboardSerializedDashboard + // normalizes serialized_dashboard to a JSON string before this runs, so it maps + // straight onto the schema's string field. vout, diags := convert.Normalize(schema.ResourceDashboard{}, vin) for _, diag := range diags { log.Debugf(ctx, "dashboard normalization diagnostic: %s", diag.Summary) } - // Marshal "serialized_dashboard" as JSON if it is set in the input but not in the output. - vout, err = marshalSerializedDashboard(vin, vout) - if err != nil { - return dyn.InvalidValue, err - } - // Drop the "file_path" field. It's always inlined into "serialized_dashboard". vout, err = dyn.DropKeys(vout, []string{"file_path"}) if err != nil { diff --git a/bundle/deploy/terraform/tfdyn/convert_dashboard_test.go b/bundle/deploy/terraform/tfdyn/convert_dashboard_test.go index d8f05dc76c1..44b36abd32a 100644 --- a/bundle/deploy/terraform/tfdyn/convert_dashboard_test.go +++ b/bundle/deploy/terraform/tfdyn/convert_dashboard_test.go @@ -77,17 +77,12 @@ func TestConvertDashboardSerializedDashboardString(t *testing.T) { }) } -func TestConvertDashboardSerializedDashboardAny(t *testing.T) { +func TestConvertDashboardDropsFilePath(t *testing.T) { + // ConfigureDashboardSerializedDashboard reads file_path into serialized_dashboard + // but keeps file_path around, so the converter sees both and must drop file_path. src := resources.Dashboard{ DashboardConfig: resources.DashboardConfig{ - SerializedDashboard: map[string]any{ - "pages": []map[string]any{ - { - "displayName": "New Page", - "layout": []map[string]any{}, - }, - }, - }, + SerializedDashboard: `{ "json": true }`, }, FilePath: "some/path/to/dashboard.lvdash.json", } @@ -102,7 +97,7 @@ func TestConvertDashboardSerializedDashboardAny(t *testing.T) { // Assert that the "serialized_dashboard" is included. assert.Subset(t, out.Dashboard["my_dashboard"], map[string]any{ - "serialized_dashboard": `{"pages":[{"displayName":"New Page","layout":[]}]}`, + "serialized_dashboard": `{ "json": true }`, }) // Assert that the "file_path" is dropped. diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index 36eaa3e27df..41e0aac7e26 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -259,18 +259,10 @@ var testConfig map[string]any = map[string]any{ DisplayName: "my-dashboard", ParentPath: "/Workspace/Users/user@example.com", WarehouseId: "test-warehouse-id", - // Use []any/map[string]any to mirror how this any-typed field is - // populated in production (JSON/dyn decoding); a typed []map[string]any - // can never come out of that path. - SerializedDashboard: map[string]any{ - "pages": []any{ - map[string]any{ - "name": "page1", - "displayName": "Page 1", - "pageType": "PAGE_TYPE_CANVAS", - }, - }, - }, + // ConfigureDashboardSerializedDashboard normalizes this any-typed field + // to a JSON string before the deploy engine runs, so it is always a + // string by the time DoCreate/DoUpdate see it. + SerializedDashboard: `{"pages":[{"name":"page1","displayName":"Page 1","pageType":"PAGE_TYPE_CANVAS"}]}`, DatasetCatalog: "main", DatasetSchema: "myschema", diff --git a/bundle/direct/dresources/dashboard.go b/bundle/direct/dresources/dashboard.go index aaf1feea616..8b634f72a39 100644 --- a/bundle/direct/dresources/dashboard.go +++ b/bundle/direct/dresources/dashboard.go @@ -2,7 +2,6 @@ package dresources import ( "context" - "encoding/json" "fmt" "path" "slices" @@ -237,17 +236,15 @@ func prepareDashboardRequest(config *DashboardState) (dashboards.Dashboard, erro // Thus we need to filter such fields out. ForceSendFields: utils.FilterFields[dashboards.Dashboard](config.ForceSendFields), } - v := config.SerializedDashboard - if serializedDashboard, ok := v.(string); ok { - // If serialized dashboard is already a string, we can use it directly. - dashboard.SerializedDashboard = serializedDashboard - } else if v != nil { - // If it's inlined in the bundle config as a map, we need to marshal it to a string. - b, err := json.Marshal(v) - if err != nil { - return dashboards.Dashboard{}, fmt.Errorf("failed to marshal serialized dashboard: %w", err) - } - dashboard.SerializedDashboard = string(b) + // ConfigureDashboardSerializedDashboard normalizes serialized_dashboard to a JSON + // string (read from file_path, or marshalled from inline YAML) before the deploy + // engine runs, so the value is always a string or unset by this point. + switch v := config.SerializedDashboard.(type) { + case nil: + case string: + dashboard.SerializedDashboard = v + default: + return dashboards.Dashboard{}, fmt.Errorf("internal error: serialized_dashboard should have been normalized to a string, got %T", v) } return dashboard, nil } From 9aea1255db848f549fdc1e82b2dda6d5115db4cf Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 19:56:53 +0000 Subject: [PATCH 08/24] Address review on serialized-inline: replace_ids.py and READPLAN - Use replace_ids.py instead of bundle summary + add_repl for the dashboard ID. - Add the READPLAN matrix; the create request is identical in-memory and from a saved plan. Co-authored-by: Isaac --- .../resources/dashboards/serialized-inline/out.test.toml | 1 + .../resources/dashboards/serialized-inline/output.txt | 6 ++---- .../bundle/resources/dashboards/serialized-inline/script | 8 +++----- .../resources/dashboards/serialized-inline/test.toml | 3 +++ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/out.test.toml b/acceptance/bundle/resources/dashboards/serialized-inline/out.test.toml index cef45fe553a..b1bef937a27 100644 --- a/acceptance/bundle/resources/dashboards/serialized-inline/out.test.toml +++ b/acceptance/bundle/resources/dashboards/serialized-inline/out.test.toml @@ -1,3 +1,4 @@ Cloud = true EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] EnvMatrix.DMS = ["", "true"] +EnvMatrix.READPLAN = ["", "1"] diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/output.txt b/acceptance/bundle/resources/dashboards/serialized-inline/output.txt index 5fbda9f2d9e..f469ff494a4 100644 --- a/acceptance/bundle/resources/dashboards/serialized-inline/output.txt +++ b/acceptance/bundle/resources/dashboards/serialized-inline/output.txt @@ -3,11 +3,9 @@ create dashboards.inline_dash Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged - ->>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dashboard-serialized-inline-[UNIQUE_NAME]/default/files... Created dashboards.inline_dash -Files: 6 uploaded, 0 deleted +Files: 7 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged >>> print_requests.py //api/2.0/lakeview/dashboards @@ -23,7 +21,7 @@ Resources: 1 created, 0 changed, 0 deleted, 0 unchanged } { "method": "POST", - "path": "/api/2.0/lakeview/dashboards/[DASHBOARD_ID]/published", + "path": "/api/2.0/lakeview/dashboards/[INLINE_DASH_ID]/published", "body": { "embed_credentials": false, "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/script b/acceptance/bundle/resources/dashboards/serialized-inline/script index 1cbe3b8b0df..dc1b6d7a15b 100644 --- a/acceptance/bundle/resources/dashboards/serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/serialized-inline/script @@ -8,11 +8,9 @@ cleanup() { trap cleanup EXIT trace $CLI bundle plan -trace $CLI bundle deploy - -# Stabilize the non-deterministic dashboard ID in recorded requests. -dashboard_id=$($CLI bundle summary --output json | jq -r '.resources.dashboards.inline_dash.id') -add_repl "$dashboard_id" DASHBOARD_ID +$CLI bundle plan -o json > tmp.plan.json +$CLI bundle deploy $(readplanarg tmp.plan.json) +replace_ids.py # Prove the inline YAML map reaches the API as a JSON string. trace print_requests.py //api/2.0/lakeview/dashboards diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/test.toml b/acceptance/bundle/resources/dashboards/serialized-inline/test.toml index 17383eac468..6f91063a6ee 100644 --- a/acceptance/bundle/resources/dashboards/serialized-inline/test.toml +++ b/acceptance/bundle/resources/dashboards/serialized-inline/test.toml @@ -1,3 +1,6 @@ Ignore = [ "databricks.yml", + "tmp.plan.json", ] + +EnvMatrix.READPLAN = ["", "1"] From 2d72fb21b4d1b6b4be779f2ea4adbf862be26428 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 20:01:08 +0000 Subject: [PATCH 09/24] remove comment --- .../bundle/resources/dashboards/change-serialized-inline/script | 1 - 1 file changed, 1 deletion(-) diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/script b/acceptance/bundle/resources/dashboards/change-serialized-inline/script index 2ad282edd7c..89e5c8689e5 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/script @@ -14,7 +14,6 @@ add_repl "$dashboard_id" DASHBOARD_ID # Wait out eventual consistency (direct's first read 404s) before changing. retry --until "Page 1" $CLI lakeview get "$dashboard_id" > /dev/null -# Change a value inside the map; update_file.py asserts if the text is missing. update_file.py databricks.yml "Page 1" "Page One" # Should be detected as an update, not a recreate. From 722f8ebf1b331aaaef55f6ce18aa3b5c992af91a Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 20:04:36 +0000 Subject: [PATCH 10/24] Assert the change plan with contains.py Encode "update, not recreate" as an executable check instead of a prose comment. Co-authored-by: Isaac --- .../resources/dashboards/change-serialized-inline/script | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/script b/acceptance/bundle/resources/dashboards/change-serialized-inline/script index 89e5c8689e5..cc8d4af20c4 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/script @@ -16,8 +16,7 @@ retry --until "Page 1" $CLI lakeview get "$dashboard_id" > /dev/null update_file.py databricks.yml "Page 1" "Page One" -# Should be detected as an update, not a recreate. -trace $CLI bundle plan +trace $CLI bundle plan | contains.py "update" "!recreate" # Per-engine JSON plan; also the saved plan for READPLAN=1. trace $CLI bundle plan -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json From b591e5a182d9417da6241c76676f9b18dd3a4e73 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 20:11:30 +0000 Subject: [PATCH 11/24] Capture the update request per engine, not direct-only Drop the direct-only guard and record out.requests.$DATABRICKS_BUNDLE_ENGINE.txt for both engines. This documents terraform's OUTPUT_ONLY-field echoing instead of hiding it, following the repo's per-engine-file convention for divergent output. Co-authored-by: Isaac --- .../out.requests.terraform.txt | 23 +++++++++++++++++++ .../change-serialized-inline/script | 11 ++++----- 2 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.terraform.txt diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.terraform.txt b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.terraform.txt new file mode 100644 index 00000000000..641cd24882e --- /dev/null +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.requests.terraform.txt @@ -0,0 +1,23 @@ +{ + "method": "PATCH", + "path": "/api/2.0/lakeview/dashboards/[DASHBOARD_ID]", + "body": { + "create_time": "[TIMESTAMP]", + "dashboard_id": "[DASHBOARD_ID]", + "display_name": "Inline Dash", + "lifecycle_state": "ACTIVE", + "parent_path": "/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources", + "path": "/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources/Inline Dash.lvdash.json", + "serialized_dashboard": "{\"pages\":[{\"displayName\":\"Page One\",\"name\":\"page1\"}]}", + "update_time": "[TIMESTAMP]", + "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" + } +} +{ + "method": "POST", + "path": "/api/2.0/lakeview/dashboards/[DASHBOARD_ID]/published", + "body": { + "embed_credentials": false, + "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" + } +} diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/script b/acceptance/bundle/resources/dashboards/change-serialized-inline/script index cc8d4af20c4..c58380f7a21 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/script @@ -21,15 +21,12 @@ trace $CLI bundle plan | contains.py "update" "!recreate" # Per-engine JSON plan; also the saved plan for READPLAN=1. trace $CLI bundle plan -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json -# Drop initial-deploy requests so we capture only the change (direct only). -if [ "$DATABRICKS_BUNDLE_ENGINE" = "direct" ]; then - print_requests.py //api/2.0/lakeview/dashboards > /dev/null -fi +# Drop the initial-deploy requests so the capture below shows only the change. +print_requests.py //api/2.0/lakeview/dashboards > /dev/null # Deploy the change: in-memory (READPLAN="") or from the saved plan (READPLAN="1"). title "deploy the change\n" $CLI bundle deploy $(readplanarg out.plan.$DATABRICKS_BUNDLE_ENGINE.json) -if [ "$DATABRICKS_BUNDLE_ENGINE" = "direct" ]; then - print_requests.py //api/2.0/lakeview/dashboards > out.requests.direct.txt -fi +# Capture the update request per engine (terraform echoes OUTPUT_ONLY fields). +print_requests.py //api/2.0/lakeview/dashboards > out.requests.$DATABRICKS_BUNDLE_ENGINE.txt From 2806b22594b2d8e24b6c8b332f8a2d435b5f0e6e Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 20:21:51 +0000 Subject: [PATCH 12/24] Project change-serialized-inline plan golden to the asserted change The direct plan dumped remote_state (etag, timestamps, and the API's pretty-printed serialized_dashboard string), which a real workspace formats differently than the local fake, breaking the cloud run. Project the golden to just the resource action and the serialized_dashboard old/new diff; keep the full plan in a tmp file for the READPLAN=1 deploy. Co-authored-by: Isaac --- .../out.plan.direct.json | 69 +------------------ .../out.plan.terraform.json | 1 - .../change-serialized-inline/output.txt | 2 +- .../change-serialized-inline/script | 9 ++- .../change-serialized-inline/test.toml | 1 + 5 files changed, 9 insertions(+), 73 deletions(-) diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json index b9244b3ab9d..8228bcdebe1 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json @@ -1,69 +1,8 @@ { - "plan_version": 2, - "cli_version": "[CLI_VERSION]", - "lineage": "[UUID]", - "serial": 1, "plan": { "resources.dashboards.inline_dash": { "action": "update", - "new_state": { - "value": { - "display_name": "Inline Dash", - "embed_credentials": false, - "parent_path": "/Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources", - "published": true, - "serialized_dashboard": { - "pages": [ - { - "displayName": "Page One", - "name": "page1" - } - ] - }, - "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" - } - }, - "remote_state": { - "create_time": "[TIMESTAMP]", - "dashboard_id": "[DASHBOARD_ID]", - "display_name": "Inline Dash", - "embed_credentials": false, - "etag": "[NUMID]", - "lifecycle_state": "ACTIVE", - "parent_path": "/Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources", - "path": "/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources/Inline Dash.lvdash.json", - "published": true, - "serialized_dashboard": "{\"pages\":[{\"displayName\":\"Page 1\",\"name\":\"page1\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}\n", - "update_time": "[TIMESTAMP]", - "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" - }, "changes": { - "create_time": { - "action": "skip", - "reason": "spec:output_only", - "remote": "[TIMESTAMP]" - }, - "dashboard_id": { - "action": "skip", - "reason": "spec:output_only", - "remote": "[DASHBOARD_ID]" - }, - "etag": { - "action": "skip", - "reason": "custom", - "old": "[NUMID]", - "remote": "[NUMID]" - }, - "lifecycle_state": { - "action": "skip", - "reason": "spec:output_only", - "remote": "ACTIVE" - }, - "path": { - "action": "skip", - "reason": "spec:output_only", - "remote": "/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources/Inline Dash.lvdash.json" - }, "serialized_dashboard": { "action": "update", "old": { @@ -81,13 +20,7 @@ "name": "page1" } ] - }, - "remote": "{\"pages\":[{\"displayName\":\"Page 1\",\"name\":\"page1\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}\n" - }, - "update_time": { - "action": "skip", - "reason": "spec:output_only", - "remote": "[TIMESTAMP]" + } } } } diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.terraform.json b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.terraform.json index 7795272689a..8fc18e647ba 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.terraform.json +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.terraform.json @@ -1,5 +1,4 @@ { - "cli_version": "[CLI_VERSION]", "plan": { "resources.dashboards.inline_dash": { "action": "update" diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt b/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt index ca5a2744eaf..d8537af591b 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt @@ -15,7 +15,7 @@ Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged === deploy the change Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/files... Updated dashboards.inline_dash -Files: 4 uploaded, 0 deleted +Files: 5 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged >>> [CLI] bundle destroy --auto-approve diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/script b/acceptance/bundle/resources/dashboards/change-serialized-inline/script index c58380f7a21..37a92247b4f 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/script @@ -18,15 +18,18 @@ update_file.py databricks.yml "Page 1" "Page One" trace $CLI bundle plan | contains.py "update" "!recreate" -# Per-engine JSON plan; also the saved plan for READPLAN=1. -trace $CLI bundle plan -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json +# Full plan drives the READPLAN=1 deploy below. +trace $CLI bundle plan -o json > tmp.plan.json +# Golden keeps only the asserted change; a real workspace formats the dropped +# remote_state/etag/serialized_dashboard fields differently than the local fake. +jq '{plan: (.plan | map_values({action} + (if .changes then {changes: {serialized_dashboard: (.changes.serialized_dashboard | {action, old, new})}} else {} end)))}' tmp.plan.json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json # Drop the initial-deploy requests so the capture below shows only the change. print_requests.py //api/2.0/lakeview/dashboards > /dev/null # Deploy the change: in-memory (READPLAN="") or from the saved plan (READPLAN="1"). title "deploy the change\n" -$CLI bundle deploy $(readplanarg out.plan.$DATABRICKS_BUNDLE_ENGINE.json) +$CLI bundle deploy $(readplanarg tmp.plan.json) # Capture the update request per engine (terraform echoes OUTPUT_ONLY fields). print_requests.py //api/2.0/lakeview/dashboards > out.requests.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml b/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml index eb67230b265..6f91063a6ee 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml @@ -1,5 +1,6 @@ Ignore = [ "databricks.yml", + "tmp.plan.json", ] EnvMatrix.READPLAN = ["", "1"] From da5627c16dbea3f040fa656fdc8216ef24cfee6b Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 20:33:13 +0000 Subject: [PATCH 13/24] Make change-serialized-inline local-only, revert plan projection The real Lakeview API pretty-prints serialized_dashboard and returns a differently-shaped etag, which the recorded plan golden can't match on a real workspace. Mark the test Cloud=false and drop the jq projection, dumping the full plan again (stable against the local fake). Co-authored-by: Isaac --- .../out.plan.direct.json | 69 ++++++++++++++++++- .../out.plan.terraform.json | 1 + .../change-serialized-inline/out.test.toml | 2 +- .../change-serialized-inline/output.txt | 2 +- .../change-serialized-inline/script | 9 +-- .../change-serialized-inline/test.toml | 5 +- 6 files changed, 78 insertions(+), 10 deletions(-) diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json index 8228bcdebe1..b9244b3ab9d 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.direct.json @@ -1,8 +1,69 @@ { + "plan_version": 2, + "cli_version": "[CLI_VERSION]", + "lineage": "[UUID]", + "serial": 1, "plan": { "resources.dashboards.inline_dash": { "action": "update", + "new_state": { + "value": { + "display_name": "Inline Dash", + "embed_credentials": false, + "parent_path": "/Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources", + "published": true, + "serialized_dashboard": { + "pages": [ + { + "displayName": "Page One", + "name": "page1" + } + ] + }, + "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" + } + }, + "remote_state": { + "create_time": "[TIMESTAMP]", + "dashboard_id": "[DASHBOARD_ID]", + "display_name": "Inline Dash", + "embed_credentials": false, + "etag": "[NUMID]", + "lifecycle_state": "ACTIVE", + "parent_path": "/Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources", + "path": "/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources/Inline Dash.lvdash.json", + "published": true, + "serialized_dashboard": "{\"pages\":[{\"displayName\":\"Page 1\",\"name\":\"page1\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}\n", + "update_time": "[TIMESTAMP]", + "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" + }, "changes": { + "create_time": { + "action": "skip", + "reason": "spec:output_only", + "remote": "[TIMESTAMP]" + }, + "dashboard_id": { + "action": "skip", + "reason": "spec:output_only", + "remote": "[DASHBOARD_ID]" + }, + "etag": { + "action": "skip", + "reason": "custom", + "old": "[NUMID]", + "remote": "[NUMID]" + }, + "lifecycle_state": { + "action": "skip", + "reason": "spec:output_only", + "remote": "ACTIVE" + }, + "path": { + "action": "skip", + "reason": "spec:output_only", + "remote": "/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/resources/Inline Dash.lvdash.json" + }, "serialized_dashboard": { "action": "update", "old": { @@ -20,7 +81,13 @@ "name": "page1" } ] - } + }, + "remote": "{\"pages\":[{\"displayName\":\"Page 1\",\"name\":\"page1\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}\n" + }, + "update_time": { + "action": "skip", + "reason": "spec:output_only", + "remote": "[TIMESTAMP]" } } } diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.terraform.json b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.terraform.json index 8fc18e647ba..7795272689a 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.terraform.json +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.plan.terraform.json @@ -1,4 +1,5 @@ { + "cli_version": "[CLI_VERSION]", "plan": { "resources.dashboards.inline_dash": { "action": "update" diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.test.toml b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.test.toml index b1bef937a27..bf4f0e7d3c6 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/out.test.toml +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/out.test.toml @@ -1,4 +1,4 @@ -Cloud = true +Cloud = false EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] EnvMatrix.DMS = ["", "true"] EnvMatrix.READPLAN = ["", "1"] diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt b/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt index d8537af591b..ca5a2744eaf 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/output.txt @@ -15,7 +15,7 @@ Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged === deploy the change Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/change-serialized-inline-[UNIQUE_NAME]/default/files... Updated dashboards.inline_dash -Files: 5 uploaded, 0 deleted +Files: 4 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged >>> [CLI] bundle destroy --auto-approve diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/script b/acceptance/bundle/resources/dashboards/change-serialized-inline/script index 37a92247b4f..c58380f7a21 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/script @@ -18,18 +18,15 @@ update_file.py databricks.yml "Page 1" "Page One" trace $CLI bundle plan | contains.py "update" "!recreate" -# Full plan drives the READPLAN=1 deploy below. -trace $CLI bundle plan -o json > tmp.plan.json -# Golden keeps only the asserted change; a real workspace formats the dropped -# remote_state/etag/serialized_dashboard fields differently than the local fake. -jq '{plan: (.plan | map_values({action} + (if .changes then {changes: {serialized_dashboard: (.changes.serialized_dashboard | {action, old, new})}} else {} end)))}' tmp.plan.json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json +# Per-engine JSON plan; also the saved plan for READPLAN=1. +trace $CLI bundle plan -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json # Drop the initial-deploy requests so the capture below shows only the change. print_requests.py //api/2.0/lakeview/dashboards > /dev/null # Deploy the change: in-memory (READPLAN="") or from the saved plan (READPLAN="1"). title "deploy the change\n" -$CLI bundle deploy $(readplanarg tmp.plan.json) +$CLI bundle deploy $(readplanarg out.plan.$DATABRICKS_BUNDLE_ENGINE.json) # Capture the update request per engine (terraform echoes OUTPUT_ONLY fields). print_requests.py //api/2.0/lakeview/dashboards > out.requests.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml b/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml index 6f91063a6ee..5bb2a87bbbc 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml @@ -1,6 +1,9 @@ +# Local only: the real Lakeview API pretty-prints serialized_dashboard and returns a +# differently-shaped etag, which the recorded plan golden can't match. +Cloud = false + Ignore = [ "databricks.yml", - "tmp.plan.json", ] EnvMatrix.READPLAN = ["", "1"] From 94fc3938cd24dda3df2b78150520256cb691fe59 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 20:34:48 +0000 Subject: [PATCH 14/24] humanize comment --- .../resources/dashboards/change-serialized-inline/test.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml b/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml index 5bb2a87bbbc..5cab5faff69 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/test.toml @@ -1,5 +1,6 @@ -# Local only: the real Lakeview API pretty-prints serialized_dashboard and returns a -# differently-shaped etag, which the recorded plan golden can't match. +# the real API returns a pretty printed JSON output which causes +# the test to fail, since we are testing CLI only feature, we can set to +# Cloud = true Cloud = false Ignore = [ From 88bd16918cc2daac84b2576fbdda3879d6aef988 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Tue, 1 Sep 2026 21:04:01 +0000 Subject: [PATCH 15/24] Fix Windows: unset MSYS_NO_PATHCONV so bare python helpers resolve The inherited MSYS_NO_PATHCONV=1 stops Git-Bash from converting a helper script's own /c/... path when handing it to python3.exe, so replace_ids.py / update_file.py / print_requests.py fail to open on Windows. These tests pass no absolute paths to the CLI (print_requests.py uses the // form), so unset it. Co-authored-by: Isaac --- .../resources/dashboards/change-serialized-inline/script | 3 +++ .../bundle/resources/dashboards/serialized-inline/script | 3 +++ 2 files changed, 6 insertions(+) diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/script b/acceptance/bundle/resources/dashboards/change-serialized-inline/script index c58380f7a21..cc0783834fb 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/script @@ -1,3 +1,6 @@ +# Inherited MSYS_NO_PATHCONV breaks bare python helpers on Windows; this test doesn't need it. +unset MSYS_NO_PATHCONV + cleanup() { trace $CLI bundle destroy --auto-approve rm -f out.requests.txt diff --git a/acceptance/bundle/resources/dashboards/serialized-inline/script b/acceptance/bundle/resources/dashboards/serialized-inline/script index dc1b6d7a15b..78c6503704b 100644 --- a/acceptance/bundle/resources/dashboards/serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/serialized-inline/script @@ -1,3 +1,6 @@ +# Inherited MSYS_NO_PATHCONV breaks bare python helpers on Windows; this test doesn't need it. +unset MSYS_NO_PATHCONV + envsubst < databricks.yml.tmpl > databricks.yml cleanup() { From 8766edc3aa11c861835edd46d33ce68b444d5519 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Wed, 2 Sep 2026 08:01:03 +0000 Subject: [PATCH 16/24] address comments --- .../dashboards/change-serialized-inline/script | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/acceptance/bundle/resources/dashboards/change-serialized-inline/script b/acceptance/bundle/resources/dashboards/change-serialized-inline/script index cc0783834fb..03b8c2ac46b 100644 --- a/acceptance/bundle/resources/dashboards/change-serialized-inline/script +++ b/acceptance/bundle/resources/dashboards/change-serialized-inline/script @@ -11,25 +11,21 @@ trap cleanup EXIT envsubst < databricks.yml.tmpl > databricks.yml trace $CLI bundle deploy +rm out.requests.txt + dashboard_id=$($CLI bundle summary --output json | jq -r '.resources.dashboards.inline_dash.id') add_repl "$dashboard_id" DASHBOARD_ID -# Wait out eventual consistency (direct's first read 404s) before changing. +# Wait out eventual consistency before changing. retry --until "Page 1" $CLI lakeview get "$dashboard_id" > /dev/null update_file.py databricks.yml "Page 1" "Page One" trace $CLI bundle plan | contains.py "update" "!recreate" -# Per-engine JSON plan; also the saved plan for READPLAN=1. trace $CLI bundle plan -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json -# Drop the initial-deploy requests so the capture below shows only the change. -print_requests.py //api/2.0/lakeview/dashboards > /dev/null - -# Deploy the change: in-memory (READPLAN="") or from the saved plan (READPLAN="1"). title "deploy the change\n" $CLI bundle deploy $(readplanarg out.plan.$DATABRICKS_BUNDLE_ENGINE.json) -# Capture the update request per engine (terraform echoes OUTPUT_ONLY fields). print_requests.py //api/2.0/lakeview/dashboards > out.requests.$DATABRICKS_BUNDLE_ENGINE.txt From b3232d4b8ddd2ef720c6d4eced2fa34fe96f99c4 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Wed, 2 Sep 2026 09:10:26 +0000 Subject: [PATCH 17/24] trim strings --- .../configure_dashboards_serialized_dashboard.go | 9 ++------- bundle/direct/dresources/all_test.go | 9 +++------ bundle/direct/dresources/dashboard.go | 3 --- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go index 4cab2c9ff9c..a6fe8e1e452 100644 --- a/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go +++ b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go @@ -52,8 +52,7 @@ func (c configureDashboardSerializedDashboard) Apply(_ context.Context, b *bundl }) return v, nil } - // Note: the Terraform resource supports "file_path" natively, but we read the contents of the dashboard here - // to be able to read file contents in Databricks Workspace (reading a dashboard file via file system fails there) + contents, err := b.SyncRoot.ReadFile(filePath) if err != nil { return dyn.InvalidValue, fmt.Errorf("failed to read serialized dashboard from file_path %s: %w", filePath, err) @@ -61,11 +60,7 @@ func (c configureDashboardSerializedDashboard) Apply(_ context.Context, b *bundl return dyn.Set(v, serializedDashboardFieldName, dyn.V(string(contents))) } - // Marshal an inline structured serialized_dashboard to a JSON string so - // both config-side and state-side carry the same plain string. - // Otherwise YAML decodes small ints as Go `int` while state JSON - // round-trip decodes them as `float64`, and structdiff reports - // false drift on every plan. + // Marshal an inline structured serialized_dashboard to a JSON string switch sd.Kind() { case dyn.KindInvalid, dyn.KindNil, dyn.KindString: // KindInvalid means serialized_dashboard is absent (neither it nor diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index 41e0aac7e26..95742506887 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -256,12 +256,9 @@ var testConfig map[string]any = map[string]any{ "dashboards": &resources.Dashboard{ DashboardConfig: resources.DashboardConfig{ - DisplayName: "my-dashboard", - ParentPath: "/Workspace/Users/user@example.com", - WarehouseId: "test-warehouse-id", - // ConfigureDashboardSerializedDashboard normalizes this any-typed field - // to a JSON string before the deploy engine runs, so it is always a - // string by the time DoCreate/DoUpdate see it. + DisplayName: "my-dashboard", + ParentPath: "/Workspace/Users/user@example.com", + WarehouseId: "test-warehouse-id", SerializedDashboard: `{"pages":[{"name":"page1","displayName":"Page 1","pageType":"PAGE_TYPE_CANVAS"}]}`, DatasetCatalog: "main", diff --git a/bundle/direct/dresources/dashboard.go b/bundle/direct/dresources/dashboard.go index 8b634f72a39..5103858974a 100644 --- a/bundle/direct/dresources/dashboard.go +++ b/bundle/direct/dresources/dashboard.go @@ -236,9 +236,6 @@ func prepareDashboardRequest(config *DashboardState) (dashboards.Dashboard, erro // Thus we need to filter such fields out. ForceSendFields: utils.FilterFields[dashboards.Dashboard](config.ForceSendFields), } - // ConfigureDashboardSerializedDashboard normalizes serialized_dashboard to a JSON - // string (read from file_path, or marshalled from inline YAML) before the deploy - // engine runs, so the value is always a string or unset by this point. switch v := config.SerializedDashboard.(type) { case nil: case string: From 5b66b758dadf89462edfa7c93c174ec1a502cbe3 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Wed, 2 Sep 2026 09:15:33 +0000 Subject: [PATCH 18/24] Retrigger CI Co-authored-by: Isaac From 245df9957b10bb3a76fb9b265b98ee4b5f0cf8e1 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Wed, 2 Sep 2026 12:39:37 +0000 Subject: [PATCH 19/24] restore comment --- .../configure_dashboards_serialized_dashboard.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go index a6fe8e1e452..cadabf27e51 100644 --- a/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go +++ b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go @@ -37,6 +37,9 @@ func (c configureDashboardSerializedDashboard) Apply(_ context.Context, b *bundl // Configure serialized_dashboard field for all dashboards. err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) { return dyn.MapByPattern(v, pattern, func(p dyn.Path, v dyn.Value) (dyn.Value, error) { + // Include "serialized_dashboard" field if "file_path" is set. + // Note: the Terraform resource supports "file_path" natively, but we read the contents of the dashboard here + // to be able to read file contents in Databricks Workspace (reading a dashboard file via file system fails there) filePath, hasFilePath := v.Get(filePathFieldName).AsString() sd := v.Get(serializedDashboardFieldName) From bf16b52ff49ca49710a7f100c64aab719207aa4d Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Wed, 2 Sep 2026 16:40:04 +0000 Subject: [PATCH 20/24] remove sequence --- .../configure_dashboards_serialized_dashboard.go | 4 ++-- .../configure_genie_space_serialized_space_test.go | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go index cadabf27e51..a17e5c6b07c 100644 --- a/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go +++ b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard.go @@ -69,7 +69,7 @@ func (c configureDashboardSerializedDashboard) Apply(_ context.Context, b *bundl // KindInvalid means serialized_dashboard is absent (neither it nor // file_path is set); leave it for backend validation to reject. return v, nil - case dyn.KindMap, dyn.KindSequence: + case dyn.KindMap: jsonBytes, err := json.Marshal(sd.AsAny()) if err != nil { return dyn.InvalidValue, fmt.Errorf("failed to marshal inline serialized_dashboard: %w", err) @@ -78,7 +78,7 @@ func (c configureDashboardSerializedDashboard) Apply(_ context.Context, b *bundl default: diags = diags.Append(diag.Diagnostic{ Severity: diag.Error, - Summary: fmt.Sprintf("serialized_dashboard must be a string, map, or sequence, got %s", sd.Kind()), + Summary: fmt.Sprintf("serialized_dashboard must be a string or map, got %s", sd.Kind()), Locations: sd.Locations(), }) return v, nil diff --git a/bundle/config/mutator/resourcemutator/configure_genie_space_serialized_space_test.go b/bundle/config/mutator/resourcemutator/configure_genie_space_serialized_space_test.go index 19c1b685f10..b15872bf106 100644 --- a/bundle/config/mutator/resourcemutator/configure_genie_space_serialized_space_test.go +++ b/bundle/config/mutator/resourcemutator/configure_genie_space_serialized_space_test.go @@ -70,8 +70,14 @@ func TestConfigureGenieSpaceSerializedSpace(t *testing.T) { name: "non-structured serialized_space is rejected", setSerialized: true, serializedSpace: true, - wantErr: "serialized_space must be a string, map, or sequence, got bool", + wantErr: "serialized_space must be a string or map, got bool", }, + { + name: "inline sequence is rejected", + setSerialized: true, + serializedSpace: []any{map[string]any{"version": 1}}, + wantErr: "serialized_space must be a string or map, got sequence", + } { name: "unreadable file_path is an error", filePath: "does_not_exist.json", From 49c7ce56857d0a3752b5bc30dabae36beb27141c Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Wed, 2 Sep 2026 16:41:07 +0000 Subject: [PATCH 21/24] revert wrong commit --- .../configure_genie_space_serialized_space_test.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/bundle/config/mutator/resourcemutator/configure_genie_space_serialized_space_test.go b/bundle/config/mutator/resourcemutator/configure_genie_space_serialized_space_test.go index b15872bf106..19c1b685f10 100644 --- a/bundle/config/mutator/resourcemutator/configure_genie_space_serialized_space_test.go +++ b/bundle/config/mutator/resourcemutator/configure_genie_space_serialized_space_test.go @@ -70,14 +70,8 @@ func TestConfigureGenieSpaceSerializedSpace(t *testing.T) { name: "non-structured serialized_space is rejected", setSerialized: true, serializedSpace: true, - wantErr: "serialized_space must be a string or map, got bool", + wantErr: "serialized_space must be a string, map, or sequence, got bool", }, - { - name: "inline sequence is rejected", - setSerialized: true, - serializedSpace: []any{map[string]any{"version": 1}}, - wantErr: "serialized_space must be a string or map, got sequence", - } { name: "unreadable file_path is an error", filePath: "does_not_exist.json", From f40a8a0157ea60a28359f194500f18a2253a2114 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Wed, 2 Sep 2026 16:42:31 +0000 Subject: [PATCH 22/24] update test --- .../configure_dashboards_serialized_dashboard_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard_test.go b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard_test.go index 7800e4741a5..41111291972 100644 --- a/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard_test.go +++ b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard_test.go @@ -70,8 +70,14 @@ func TestConfigureDashboardSerializedDashboard(t *testing.T) { name: "non-structured serialized_dashboard is rejected", setSerialized: true, serializedDashboard: true, - wantErr: "serialized_dashboard must be a string, map, or sequence, got bool", + wantErr: "serialized_dashboard must be a string or map, got bool", }, + { + name: "inline sequence is rejected", + setSerialized: true, + serializedDashboard: []any{map[string]any{"version": 1}}, + wantErr: "serialized_dashboard must be a string or map, got sequence", + } { name: "unreadable file_path is an error", filePath: "does_not_exist.json", From 3a89112e56f7faff9c009f60b9feebed6481d50e Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Wed, 2 Sep 2026 16:44:32 +0000 Subject: [PATCH 23/24] keep older content --- bundle/deploy/terraform/tfdyn/convert_dashboard_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundle/deploy/terraform/tfdyn/convert_dashboard_test.go b/bundle/deploy/terraform/tfdyn/convert_dashboard_test.go index 44b36abd32a..aa23b90df78 100644 --- a/bundle/deploy/terraform/tfdyn/convert_dashboard_test.go +++ b/bundle/deploy/terraform/tfdyn/convert_dashboard_test.go @@ -82,7 +82,7 @@ func TestConvertDashboardDropsFilePath(t *testing.T) { // but keeps file_path around, so the converter sees both and must drop file_path. src := resources.Dashboard{ DashboardConfig: resources.DashboardConfig{ - SerializedDashboard: `{ "json": true }`, + SerializedDashboard: `{"pages":[{"displayName":"New Page","layout":[]}]}`, }, FilePath: "some/path/to/dashboard.lvdash.json", } @@ -97,7 +97,7 @@ func TestConvertDashboardDropsFilePath(t *testing.T) { // Assert that the "serialized_dashboard" is included. assert.Subset(t, out.Dashboard["my_dashboard"], map[string]any{ - "serialized_dashboard": `{ "json": true }`, + "serialized_dashboard": `{"pages":[{"displayName":"New Page","layout":[]}]}`, }) // Assert that the "file_path" is dropped. From 85ce5e402613418b3e14d99c8050be11e7778b16 Mon Sep 17 00:00:00 2001 From: Sankalp-Mittal Date: Wed, 2 Sep 2026 16:56:58 +0000 Subject: [PATCH 24/24] Fix syntax error in dashboard serialized_dashboard mutator test The "inline sequence is rejected" case was missing the trailing comma after its struct literal, a syntax error that failed to compile the whole resourcemutator_test package and broke lint and every task test job. Co-authored-by: Isaac --- .../configure_dashboards_serialized_dashboard_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard_test.go b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard_test.go index 41111291972..b4437874b8c 100644 --- a/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard_test.go +++ b/bundle/config/mutator/resourcemutator/configure_dashboards_serialized_dashboard_test.go @@ -73,11 +73,11 @@ func TestConfigureDashboardSerializedDashboard(t *testing.T) { wantErr: "serialized_dashboard must be a string or map, got bool", }, { - name: "inline sequence is rejected", - setSerialized: true, + name: "inline sequence is rejected", + setSerialized: true, serializedDashboard: []any{map[string]any{"version": 1}}, - wantErr: "serialized_dashboard must be a string or map, got sequence", - } + wantErr: "serialized_dashboard must be a string or map, got sequence", + }, { name: "unreadable file_path is an error", filePath: "does_not_exist.json",