From 61d3d79dec2d5d2544071712b0a1154698236c02 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 5 Nov 2025 16:25:49 +0100 Subject: [PATCH 1/8] direct: Add permissions support for dashboards --- .../dashboards/create/databricks.yml | 16 ++++++ .../dashboards/create/out.plan.direct.json | 49 +++++++++++++++++++ .../dashboards/create/out.plan.terraform.json | 7 +++ .../create/out.requests.deploy.direct.json | 24 +++++++++ .../create/out.requests.deploy.terraform.json | 24 +++++++++ .../create/out.requests.destroy.direct.json | 0 .../out.requests.destroy.terraform.json | 5 ++ .../dashboards/create/out.test.toml | 6 +++ .../permissions/dashboards/create/output.txt | 35 +++++++++++++ .../permissions/dashboards/create/script | 20 ++++++++ .../permissions/dashboards/test.toml | 3 ++ bundle/direct/dresources/all.go | 3 +- bundle/direct/dresources/all_test.go | 46 ++++++++++++++++- libs/testserver/dashboards.go | 15 +++--- 14 files changed, 245 insertions(+), 8 deletions(-) create mode 100644 acceptance/bundle/resources/permissions/dashboards/create/databricks.yml create mode 100644 acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json create mode 100644 acceptance/bundle/resources/permissions/dashboards/create/out.plan.terraform.json create mode 100644 acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.direct.json create mode 100644 acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.terraform.json create mode 100644 acceptance/bundle/resources/permissions/dashboards/create/out.requests.destroy.direct.json create mode 100644 acceptance/bundle/resources/permissions/dashboards/create/out.requests.destroy.terraform.json create mode 100644 acceptance/bundle/resources/permissions/dashboards/create/out.test.toml create mode 100644 acceptance/bundle/resources/permissions/dashboards/create/output.txt create mode 100644 acceptance/bundle/resources/permissions/dashboards/create/script create mode 100644 acceptance/bundle/resources/permissions/dashboards/test.toml diff --git a/acceptance/bundle/resources/permissions/dashboards/create/databricks.yml b/acceptance/bundle/resources/permissions/dashboards/create/databricks.yml new file mode 100644 index 0000000000..e2165bcdf3 --- /dev/null +++ b/acceptance/bundle/resources/permissions/dashboards/create/databricks.yml @@ -0,0 +1,16 @@ +resources: + dashboards: + foo: + display_name: test-dashboard + warehouse_id: 123abc + parent_path: /Users/tester@databricks.com + serialized_dashboard: '{"pages":[{"name":"page1","displayName":"Page 1"}]}' + permissions: + - level: CAN_READ + user_name: viewer@example.com + - level: CAN_MANAGE + group_name: data-team + - level: CAN_MANAGE + service_principal_name: f37d18cd-98a8-4db5-8112-12dd0a6bfe38 + - level: CAN_MANAGE + user_name: tester@databricks.com diff --git a/acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json b/acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json new file mode 100644 index 0000000000..a7370e099c --- /dev/null +++ b/acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json @@ -0,0 +1,49 @@ +{ + "plan": { + "resources.dashboards.foo": { + "action": "create", + "new_state": { + "config": { + "display_name": "test-dashboard", + "parent_path": "/Users/[USERNAME]", + "warehouse_id": "123abc" + } + } + }, + "resources.dashboards.foo.permissions": { + "depends_on": [ + { + "node": "resources.dashboards.foo", + "label": "${resources.dashboards.foo.id}" + } + ], + "action": "create", + "new_state": { + "config": { + "object_id": "", + "permissions": [ + { + "permission_level": "CAN_READ", + "user_name": "viewer@example.com" + }, + { + "group_name": "data-team", + "permission_level": "CAN_MANAGE" + }, + { + "permission_level": "CAN_MANAGE", + "service_principal_name": "[UUID]" + }, + { + "permission_level": "CAN_MANAGE", + "user_name": "[USERNAME]" + } + ] + }, + "vars": { + "object_id": "/dashboards/${resources.dashboards.foo.id}" + } + } + } + } +} diff --git a/acceptance/bundle/resources/permissions/dashboards/create/out.plan.terraform.json b/acceptance/bundle/resources/permissions/dashboards/create/out.plan.terraform.json new file mode 100644 index 0000000000..ff37f53606 --- /dev/null +++ b/acceptance/bundle/resources/permissions/dashboards/create/out.plan.terraform.json @@ -0,0 +1,7 @@ +{ + "plan": { + "resources.dashboards.foo": { + "action": "create" + } + } +} diff --git a/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.direct.json b/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.direct.json new file mode 100644 index 0000000000..4bfa48388b --- /dev/null +++ b/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.direct.json @@ -0,0 +1,24 @@ +{ + "method": "PUT", + "path": "/api/2.0/permissions/dashboards/[DASHBOARD_ID]", + "body": { + "access_control_list": [ + { + "permission_level": "CAN_READ", + "user_name": "viewer@example.com" + }, + { + "group_name": "data-team", + "permission_level": "CAN_MANAGE" + }, + { + "permission_level": "CAN_MANAGE", + "service_principal_name": "[UUID]" + }, + { + "permission_level": "CAN_MANAGE", + "user_name": "[USERNAME]" + } + ] + } +} diff --git a/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.terraform.json b/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.terraform.json new file mode 100644 index 0000000000..38e4b61bb4 --- /dev/null +++ b/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.terraform.json @@ -0,0 +1,24 @@ +{ + "method": "PUT", + "path": "/api/2.0/permissions/dashboards/[DASHBOARD_ID]", + "body": { + "access_control_list": [ + { + "permission_level": "CAN_READ", + "user_name": "viewer@example.com" + }, + { + "permission_level": "CAN_MANAGE", + "service_principal_name": "[UUID]" + }, + { + "group_name": "data-team", + "permission_level": "CAN_MANAGE" + }, + { + "permission_level": "CAN_MANAGE", + "user_name": "[USERNAME]" + } + ] + } +} diff --git a/acceptance/bundle/resources/permissions/dashboards/create/out.requests.destroy.direct.json b/acceptance/bundle/resources/permissions/dashboards/create/out.requests.destroy.direct.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/acceptance/bundle/resources/permissions/dashboards/create/out.requests.destroy.terraform.json b/acceptance/bundle/resources/permissions/dashboards/create/out.requests.destroy.terraform.json new file mode 100644 index 0000000000..317d4dbc93 --- /dev/null +++ b/acceptance/bundle/resources/permissions/dashboards/create/out.requests.destroy.terraform.json @@ -0,0 +1,5 @@ +{ + "method": "PUT", + "path": "/api/2.0/permissions/dashboards/[DASHBOARD_ID]", + "body": {} +} diff --git a/acceptance/bundle/resources/permissions/dashboards/create/out.test.toml b/acceptance/bundle/resources/permissions/dashboards/create/out.test.toml new file mode 100644 index 0000000000..6feb8784c8 --- /dev/null +++ b/acceptance/bundle/resources/permissions/dashboards/create/out.test.toml @@ -0,0 +1,6 @@ +Local = true +Cloud = false +RequiresWarehouse = true + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resources/permissions/dashboards/create/output.txt b/acceptance/bundle/resources/permissions/dashboards/create/output.txt new file mode 100644 index 0000000000..91d2f9e3de --- /dev/null +++ b/acceptance/bundle/resources/permissions/dashboards/create/output.txt @@ -0,0 +1,35 @@ + +>>> [CLI] bundle validate -o json +[ + { + "level": "CAN_READ", + "user_name": "viewer@example.com" + }, + { + "group_name": "data-team", + "level": "CAN_MANAGE" + }, + { + "level": "CAN_MANAGE", + "service_principal_name": "[UUID]" + }, + { + "level": "CAN_MANAGE", + "user_name": "[USERNAME]" + } +] + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete dashboard foo + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default + +Deleting files... +Destroy complete! diff --git a/acceptance/bundle/resources/permissions/dashboards/create/script b/acceptance/bundle/resources/permissions/dashboards/create/script new file mode 100644 index 0000000000..05cc08cf1c --- /dev/null +++ b/acceptance/bundle/resources/permissions/dashboards/create/script @@ -0,0 +1,20 @@ +trace $CLI bundle validate -o json | jq .resources.$RESOURCE.foo.permissions +rm out.requests.txt + +$CLI bundle debug plan > out.plan.$DATABRICKS_BUNDLE_ENGINE.json + +print_requests() { + jq -c < out.requests.txt | jq 'select(.method != "GET" and (.path | contains("permissions")))' + rm out.requests.txt +} + +rm out.requests.txt +trace $CLI bundle deploy + +dashboard_id=$($CLI bundle summary --output json | jq -r '.resources.dashboards.foo.id') +echo "$dashboard_id:DASHBOARD_ID" >> ACC_REPLS + +print_requests > out.requests.deploy.$DATABRICKS_BUNDLE_ENGINE.json + +trace $CLI bundle destroy --auto-approve +print_requests > out.requests.destroy.$DATABRICKS_BUNDLE_ENGINE.json diff --git a/acceptance/bundle/resources/permissions/dashboards/test.toml b/acceptance/bundle/resources/permissions/dashboards/test.toml new file mode 100644 index 0000000000..45da7b6ba5 --- /dev/null +++ b/acceptance/bundle/resources/permissions/dashboards/test.toml @@ -0,0 +1,3 @@ +RequiresWarehouse = true + +Env.RESOURCE = "dashboards" # for ../_script diff --git a/bundle/direct/dresources/all.go b/bundle/direct/dresources/all.go index d87549e81d..43efabf2c2 100644 --- a/bundle/direct/dresources/all.go +++ b/bundle/direct/dresources/all.go @@ -19,14 +19,15 @@ var SupportedResources = map[string]any{ "database_catalogs": (*ResourceDatabaseCatalog)(nil), "synced_database_tables": (*ResourceSyncedDatabaseTable)(nil), "alerts": (*ResourceAlert)(nil), + "dashboards": (*ResourceDashboard)(nil), "clusters": (*ResourceCluster)(nil), "registered_models": (*ResourceRegisteredModel)(nil), - "dashboards": (*ResourceDashboard)(nil), // Permissions "jobs.permissions": (*ResourcePermissions)(nil), "pipelines.permissions": (*ResourcePermissions)(nil), "apps.permissions": (*ResourcePermissions)(nil), + "dashboards.permissions": (*ResourcePermissions)(nil), "clusters.permissions": (*ResourcePermissions)(nil), "database_instances.permissions": (*ResourcePermissions)(nil), "experiments.permissions": (*ResourcePermissions)(nil), diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index 230463ce74..a199a20995 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -19,6 +19,7 @@ import ( "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/service/apps" "github.com/databricks/databricks-sdk-go/service/catalog" + "github.com/databricks/databricks-sdk-go/service/dashboards" "github.com/databricks/databricks-sdk-go/service/database" "github.com/databricks/databricks-sdk-go/service/iam" "github.com/databricks/databricks-sdk-go/service/jobs" @@ -97,6 +98,16 @@ var testConfig map[string]any = map[string]any{ }, }, }, + + "dashboards": &resources.Dashboard{ + DashboardConfig: resources.DashboardConfig{ + Dashboard: dashboards.Dashboard{ + DisplayName: "my_dashboard", + SerializedDashboard: `{"pages":[{"name":"page1","displayName":"Page 1"}]}`, + WarehouseId: "warehouse123", + }, + }, + }, } type prepareWorkspace func(client *databricks.WorkspaceClient) (any, error) @@ -252,6 +263,27 @@ var testDeps = map[string]prepareWorkspace{ }}, }, nil }, + + "dashboards.permissions": func(client *databricks.WorkspaceClient) (any, error) { + resp, err := client.Lakeview.Create(context.Background(), dashboards.CreateDashboardRequest{ + Dashboard: dashboards.Dashboard{ + DisplayName: "dashboard-permissions", + SerializedDashboard: `{"pages":[{"name":"page1","displayName":"Page 1"}]}`, + WarehouseId: "warehouse123", + }, + }) + if err != nil { + return nil, err + } + + return &PermissionsState{ + ObjectID: "/dashboards/" + resp.DashboardId, + Permissions: []iam.AccessControlRequest{{ + PermissionLevel: "CAN_MANAGE", + UserName: "user@example.com", + }}, + }, nil + }, } func TestAll(t *testing.T) { @@ -340,7 +372,13 @@ func testCRUD(t *testing.T, group string, adapter *Adapter, client *databricks.W var relevantChanges []structdiff.Change for _, change := range changes { fieldName := change.Path.String() - if fieldName != "updated_at" { + // Filter out fields that are expected to change between DoRefresh and DoUpdate + // - updated_at, update_time: timestamps that change on updates + // - etag: version field that changes on updates + // - path: computed field for dashboards + // - serialized_dashboard: test server adds pageType and formatting + if fieldName != "updated_at" && fieldName != "update_time" && fieldName != "etag" && + fieldName != "path" && fieldName != "serialized_dashboard" { relevantChanges = append(relevantChanges, change) } } @@ -373,6 +411,12 @@ func testCRUD(t *testing.T, group string, adapter *Adapter, client *databricks.W // t.Logf("Testing %s v=%#v, remoteValue=%#v", path.String(), val, remoteValue) // We expect fields set explicitly to be preserved by testserver, which is true for all resources as of today. // If not true for your resource, add exception here: + + // Dashboard serialized_dashboard is modified by the server (adds pageType, reorders keys, adds newline) + if path.String() == "serialized_dashboard" { + return + } + assert.Equal(t, val, remoteValue, "path=%q\nnewState=%s\nremappedState=%s", path.String(), jsonDump(newState), jsonDump(remappedState)) })) diff --git a/libs/testserver/dashboards.go b/libs/testserver/dashboards.go index 155086c9b4..ef97c233b8 100644 --- a/libs/testserver/dashboards.go +++ b/libs/testserver/dashboards.go @@ -4,7 +4,6 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" - "fmt" "path" "strconv" "strings" @@ -60,12 +59,16 @@ func (s *FakeWorkspace) DashboardCreate(req Request) Response { } } + // Auto-create parent directory if it doesn't exist (matching WorkspaceFilesImportFile behavior) if _, ok := s.directories[dashboard.ParentPath]; !ok { - return Response{ - StatusCode: 404, - Body: map[string]string{ - "message": fmt.Sprintf("Path (%s) doesn't exist.", dashboard.ParentPath), - }, + for dir := dashboard.ParentPath; dir != "/" && dir != ""; dir = path.Dir(dir) { + if _, exists := s.directories[dir]; !exists { + s.directories[dir] = workspace.ObjectInfo{ + ObjectType: "DIRECTORY", + Path: dir, + ObjectId: nextID(), + } + } } } From 26d86e2afa2124659db0d09d3677a70401fab12c Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 5 Nov 2025 16:40:37 +0100 Subject: [PATCH 2/8] update to not require test server changes --- .../permissions/dashboards/create/databricks.yml | 2 +- .../dashboards/create/out.plan.direct.json | 2 +- .../permissions/dashboards/create/out.test.toml | 1 - .../permissions/dashboards/create/output.txt | 2 ++ .../permissions/dashboards/create/script | 5 ++++- .../resources/permissions/dashboards/test.toml | 3 --- bundle/direct/dresources/all.go | 2 +- libs/testserver/dashboards.go | 15 ++++++--------- 8 files changed, 15 insertions(+), 17 deletions(-) delete mode 100644 acceptance/bundle/resources/permissions/dashboards/test.toml diff --git a/acceptance/bundle/resources/permissions/dashboards/create/databricks.yml b/acceptance/bundle/resources/permissions/dashboards/create/databricks.yml index e2165bcdf3..66bc9946e4 100644 --- a/acceptance/bundle/resources/permissions/dashboards/create/databricks.yml +++ b/acceptance/bundle/resources/permissions/dashboards/create/databricks.yml @@ -3,7 +3,7 @@ resources: foo: display_name: test-dashboard warehouse_id: 123abc - parent_path: /Users/tester@databricks.com + parent_path: /Users/tester@databricks.com/folder1 serialized_dashboard: '{"pages":[{"name":"page1","displayName":"Page 1"}]}' permissions: - level: CAN_READ diff --git a/acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json b/acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json index a7370e099c..70934acffc 100644 --- a/acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json @@ -5,7 +5,7 @@ "new_state": { "config": { "display_name": "test-dashboard", - "parent_path": "/Users/[USERNAME]", + "parent_path": "/Users/[USERNAME]/folder1", "warehouse_id": "123abc" } } diff --git a/acceptance/bundle/resources/permissions/dashboards/create/out.test.toml b/acceptance/bundle/resources/permissions/dashboards/create/out.test.toml index 6feb8784c8..d560f1de04 100644 --- a/acceptance/bundle/resources/permissions/dashboards/create/out.test.toml +++ b/acceptance/bundle/resources/permissions/dashboards/create/out.test.toml @@ -1,6 +1,5 @@ Local = true Cloud = false -RequiresWarehouse = true [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resources/permissions/dashboards/create/output.txt b/acceptance/bundle/resources/permissions/dashboards/create/output.txt index 91d2f9e3de..267544e734 100644 --- a/acceptance/bundle/resources/permissions/dashboards/create/output.txt +++ b/acceptance/bundle/resources/permissions/dashboards/create/output.txt @@ -19,6 +19,8 @@ } ] +>>> [CLI] workspace mkdirs /Users/[USERNAME]/folder1 + >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... Deploying resources... diff --git a/acceptance/bundle/resources/permissions/dashboards/create/script b/acceptance/bundle/resources/permissions/dashboards/create/script index 05cc08cf1c..9f6f538e7e 100644 --- a/acceptance/bundle/resources/permissions/dashboards/create/script +++ b/acceptance/bundle/resources/permissions/dashboards/create/script @@ -1,4 +1,4 @@ -trace $CLI bundle validate -o json | jq .resources.$RESOURCE.foo.permissions +trace $CLI bundle validate -o json | jq .resources.dashboards.foo.permissions rm out.requests.txt $CLI bundle debug plan > out.plan.$DATABRICKS_BUNDLE_ENGINE.json @@ -8,6 +8,9 @@ print_requests() { rm out.requests.txt } +# create the parent path +trace $CLI workspace mkdirs /Users/tester@databricks.com/folder1 + rm out.requests.txt trace $CLI bundle deploy diff --git a/acceptance/bundle/resources/permissions/dashboards/test.toml b/acceptance/bundle/resources/permissions/dashboards/test.toml deleted file mode 100644 index 45da7b6ba5..0000000000 --- a/acceptance/bundle/resources/permissions/dashboards/test.toml +++ /dev/null @@ -1,3 +0,0 @@ -RequiresWarehouse = true - -Env.RESOURCE = "dashboards" # for ../_script diff --git a/bundle/direct/dresources/all.go b/bundle/direct/dresources/all.go index 43efabf2c2..421b61641a 100644 --- a/bundle/direct/dresources/all.go +++ b/bundle/direct/dresources/all.go @@ -19,9 +19,9 @@ var SupportedResources = map[string]any{ "database_catalogs": (*ResourceDatabaseCatalog)(nil), "synced_database_tables": (*ResourceSyncedDatabaseTable)(nil), "alerts": (*ResourceAlert)(nil), - "dashboards": (*ResourceDashboard)(nil), "clusters": (*ResourceCluster)(nil), "registered_models": (*ResourceRegisteredModel)(nil), + "dashboards": (*ResourceDashboard)(nil), // Permissions "jobs.permissions": (*ResourcePermissions)(nil), diff --git a/libs/testserver/dashboards.go b/libs/testserver/dashboards.go index ef97c233b8..155086c9b4 100644 --- a/libs/testserver/dashboards.go +++ b/libs/testserver/dashboards.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" + "fmt" "path" "strconv" "strings" @@ -59,16 +60,12 @@ func (s *FakeWorkspace) DashboardCreate(req Request) Response { } } - // Auto-create parent directory if it doesn't exist (matching WorkspaceFilesImportFile behavior) if _, ok := s.directories[dashboard.ParentPath]; !ok { - for dir := dashboard.ParentPath; dir != "/" && dir != ""; dir = path.Dir(dir) { - if _, exists := s.directories[dir]; !exists { - s.directories[dir] = workspace.ObjectInfo{ - ObjectType: "DIRECTORY", - Path: dir, - ObjectId: nextID(), - } - } + return Response{ + StatusCode: 404, + Body: map[string]string{ + "message": fmt.Sprintf("Path (%s) doesn't exist.", dashboard.ParentPath), + }, } } From c4063776099955f6ebfa2ef7fa718eef1d8dfb2d Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 5 Nov 2025 18:29:29 +0100 Subject: [PATCH 3/8] update test to cloud' --- acceptance/acceptance_test.go | 23 ++++++++++++++----- .../dashboards/create/databricks.yml | 16 ------------- .../dashboards/create/databricks.yml.tmpl | 19 +++++++++++++++ .../dashboards/create/out.plan.direct.json | 10 ++++---- .../create/out.requests.deploy.direct.json | 6 ++--- .../create/out.requests.deploy.terraform.json | 12 +++++----- .../dashboards/create/out.test.toml | 5 ++-- .../permissions/dashboards/create/output.txt | 12 ++++------ .../permissions/dashboards/create/script | 15 +++++++++--- .../permissions/dashboards/create/test.toml | 3 +++ 10 files changed, 73 insertions(+), 48 deletions(-) delete mode 100644 acceptance/bundle/resources/permissions/dashboards/create/databricks.yml create mode 100644 acceptance/bundle/resources/permissions/dashboards/create/databricks.yml.tmpl create mode 100644 acceptance/bundle/resources/permissions/dashboards/create/test.toml diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 7a5209601e..eef50437c0 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -145,6 +145,22 @@ func TestInprocessMode(t *testing.T) { require.Equal(t, 1, testAccept(t, true, "selftest/server")) } +// Configure replacements for environment variables we read from test environments. +func setReplsForTestEnvVars(t *testing.T, repls *testdiff.ReplacementsContext) { + envVars := []string{ + "TEST_USER_EMAIL", + "TEST_GROUP_NAME", + "TEST_SP_APPLICATION_ID", + "TEST_DEFAULT_WAREHOUSE_ID", + "TEST_INSTANCE_POOL_ID", + } + for _, envVar := range envVars { + if value := os.Getenv(envVar); value != "" { + repls.Set(value, "["+envVar+"]") + } + } +} + func testAccept(t *testing.T, inprocessMode bool, singleTest string) int { repls := testdiff.ReplacementsContext{} cwd, err := os.Getwd() @@ -234,10 +250,7 @@ func testAccept(t *testing.T, inprocessMode bool, singleTest string) int { } } - testDefaultWarehouseId := os.Getenv("TEST_DEFAULT_WAREHOUSE_ID") - if testDefaultWarehouseId != "" { - repls.Set(testDefaultWarehouseId, "[TEST_DEFAULT_WAREHOUSE_ID]") - } + setReplsForTestEnvVars(t, &repls) terraformrcPath := filepath.Join(terraformDir, ".terraformrc") t.Setenv("TF_CLI_CONFIG_FILE", terraformrcPath) @@ -252,8 +265,6 @@ func testAccept(t *testing.T, inprocessMode bool, singleTest string) int { // do it last so that full paths match first: repls.SetPath(buildDir, "[BUILD_DIR]") - repls.Set(os.Getenv("TEST_INSTANCE_POOL_ID"), "[TEST_INSTANCE_POOL_ID]") - testdiff.PrepareReplacementsDevVersion(t, &repls) testdiff.PrepareReplacementSdkVersion(t, &repls) testdiff.PrepareReplacementsGoVersion(t, &repls) diff --git a/acceptance/bundle/resources/permissions/dashboards/create/databricks.yml b/acceptance/bundle/resources/permissions/dashboards/create/databricks.yml deleted file mode 100644 index 66bc9946e4..0000000000 --- a/acceptance/bundle/resources/permissions/dashboards/create/databricks.yml +++ /dev/null @@ -1,16 +0,0 @@ -resources: - dashboards: - foo: - display_name: test-dashboard - warehouse_id: 123abc - parent_path: /Users/tester@databricks.com/folder1 - serialized_dashboard: '{"pages":[{"name":"page1","displayName":"Page 1"}]}' - permissions: - - level: CAN_READ - user_name: viewer@example.com - - level: CAN_MANAGE - group_name: data-team - - level: CAN_MANAGE - service_principal_name: f37d18cd-98a8-4db5-8112-12dd0a6bfe38 - - level: CAN_MANAGE - user_name: tester@databricks.com diff --git a/acceptance/bundle/resources/permissions/dashboards/create/databricks.yml.tmpl b/acceptance/bundle/resources/permissions/dashboards/create/databricks.yml.tmpl new file mode 100644 index 0000000000..0b9577d46d --- /dev/null +++ b/acceptance/bundle/resources/permissions/dashboards/create/databricks.yml.tmpl @@ -0,0 +1,19 @@ +bundle: + name: dashboard-perm-$UNIQUE_NAME + +resources: + dashboards: + foo: + display_name: test-dashboard-$UNIQUE_NAME + warehouse_id: $TEST_DEFAULT_WAREHOUSE_ID + parent_path: /Users/$CURRENT_USER_NAME/folder1 + serialized_dashboard: '{"pages":[{"name":"page1","displayName":"Page 1"}]}' + permissions: + - level: CAN_READ + user_name: $TEST_USER_EMAIL + - level: CAN_MANAGE + group_name: $TEST_GROUP_NAME + - level: CAN_MANAGE + service_principal_name: $TEST_SP_APPLICATION_ID + - level: CAN_MANAGE + user_name: $CURRENT_USER_NAME diff --git a/acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json b/acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json index 70934acffc..ee73865ea7 100644 --- a/acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json @@ -4,9 +4,9 @@ "action": "create", "new_state": { "config": { - "display_name": "test-dashboard", + "display_name": "test-dashboard-[UNIQUE_NAME]", "parent_path": "/Users/[USERNAME]/folder1", - "warehouse_id": "123abc" + "warehouse_id": "[TEST_DEFAULT_WAREHOUSE_ID]" } } }, @@ -24,15 +24,15 @@ "permissions": [ { "permission_level": "CAN_READ", - "user_name": "viewer@example.com" + "user_name": "[TEST_USER_EMAIL]" }, { - "group_name": "data-team", + "group_name": "[TEST_GROUP_NAME]", "permission_level": "CAN_MANAGE" }, { "permission_level": "CAN_MANAGE", - "service_principal_name": "[UUID]" + "service_principal_name": "[TEST_SP_APPLICATION_ID]" }, { "permission_level": "CAN_MANAGE", diff --git a/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.direct.json b/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.direct.json index 4bfa48388b..04c8cf8a9a 100644 --- a/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.direct.json +++ b/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.direct.json @@ -5,15 +5,15 @@ "access_control_list": [ { "permission_level": "CAN_READ", - "user_name": "viewer@example.com" + "user_name": "[TEST_USER_EMAIL]" }, { - "group_name": "data-team", + "group_name": "[TEST_GROUP_NAME]", "permission_level": "CAN_MANAGE" }, { "permission_level": "CAN_MANAGE", - "service_principal_name": "[UUID]" + "service_principal_name": "[TEST_SP_APPLICATION_ID]" }, { "permission_level": "CAN_MANAGE", diff --git a/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.terraform.json b/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.terraform.json index 38e4b61bb4..f20658a00a 100644 --- a/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.terraform.json +++ b/acceptance/bundle/resources/permissions/dashboards/create/out.requests.deploy.terraform.json @@ -3,21 +3,21 @@ "path": "/api/2.0/permissions/dashboards/[DASHBOARD_ID]", "body": { "access_control_list": [ - { - "permission_level": "CAN_READ", - "user_name": "viewer@example.com" - }, { "permission_level": "CAN_MANAGE", - "service_principal_name": "[UUID]" + "service_principal_name": "[TEST_SP_APPLICATION_ID]" }, { - "group_name": "data-team", + "group_name": "[TEST_GROUP_NAME]", "permission_level": "CAN_MANAGE" }, { "permission_level": "CAN_MANAGE", "user_name": "[USERNAME]" + }, + { + "permission_level": "CAN_READ", + "user_name": "[TEST_USER_EMAIL]" } ] } diff --git a/acceptance/bundle/resources/permissions/dashboards/create/out.test.toml b/acceptance/bundle/resources/permissions/dashboards/create/out.test.toml index d560f1de04..a50e6a7eed 100644 --- a/acceptance/bundle/resources/permissions/dashboards/create/out.test.toml +++ b/acceptance/bundle/resources/permissions/dashboards/create/out.test.toml @@ -1,5 +1,6 @@ -Local = true -Cloud = false +Local = false +Cloud = true +RequiresWarehouse = true [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resources/permissions/dashboards/create/output.txt b/acceptance/bundle/resources/permissions/dashboards/create/output.txt index 267544e734..a80a102323 100644 --- a/acceptance/bundle/resources/permissions/dashboards/create/output.txt +++ b/acceptance/bundle/resources/permissions/dashboards/create/output.txt @@ -3,15 +3,15 @@ [ { "level": "CAN_READ", - "user_name": "viewer@example.com" + "user_name": "[TEST_USER_EMAIL]" }, { - "group_name": "data-team", + "group_name": "[TEST_GROUP_NAME]", "level": "CAN_MANAGE" }, { "level": "CAN_MANAGE", - "service_principal_name": "[UUID]" + "service_principal_name": "[TEST_SP_APPLICATION_ID]" }, { "level": "CAN_MANAGE", @@ -19,10 +19,8 @@ } ] ->>> [CLI] workspace mkdirs /Users/[USERNAME]/folder1 - >>> [CLI] bundle deploy -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dashboard-perm-[UNIQUE_NAME]/default/files... Deploying resources... Updating deployment state... Deployment complete! @@ -31,7 +29,7 @@ Deployment complete! The following resources will be deleted: delete dashboard foo -All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/dashboard-perm-[UNIQUE_NAME]/default Deleting files... Destroy complete! diff --git a/acceptance/bundle/resources/permissions/dashboards/create/script b/acceptance/bundle/resources/permissions/dashboards/create/script index 9f6f538e7e..c4c5cc646a 100644 --- a/acceptance/bundle/resources/permissions/dashboards/create/script +++ b/acceptance/bundle/resources/permissions/dashboards/create/script @@ -1,3 +1,15 @@ +if [ -z "$CLOUD_ENV" ]; then + export TEST_USER_EMAIL="viewer@databricks.com" + export TEST_GROUP_NAME="data-team" + export TEST_SP_APPLICATION_ID="f37d18cd-98a8-4db5-8112-12dd0a6bfe38" + + echo "$TEST_USER_EMAIL:TEST_USER_EMAIL" >> ACC_REPLS + echo "$TEST_GROUP_NAME:TEST_GROUP_NAME" >> ACC_REPLS + echo "$TEST_SP_APPLICATION_ID:TEST_SP_APPLICATION_ID" >> ACC_REPLS +fi + +envsubst < databricks.yml.tmpl > databricks.yml + trace $CLI bundle validate -o json | jq .resources.dashboards.foo.permissions rm out.requests.txt @@ -8,9 +20,6 @@ print_requests() { rm out.requests.txt } -# create the parent path -trace $CLI workspace mkdirs /Users/tester@databricks.com/folder1 - rm out.requests.txt trace $CLI bundle deploy diff --git a/acceptance/bundle/resources/permissions/dashboards/create/test.toml b/acceptance/bundle/resources/permissions/dashboards/create/test.toml new file mode 100644 index 0000000000..eff14eed2e --- /dev/null +++ b/acceptance/bundle/resources/permissions/dashboards/create/test.toml @@ -0,0 +1,3 @@ +Local = false +RequiresWarehouse = true +Cloud = true From 9b47128b85eada7770a440047459699473ab4fee Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 5 Nov 2025 18:32:57 +0100 Subject: [PATCH 4/8] use priint requests python --- .../resources/permissions/dashboards/create/script | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/acceptance/bundle/resources/permissions/dashboards/create/script b/acceptance/bundle/resources/permissions/dashboards/create/script index c4c5cc646a..d9fbbde1db 100644 --- a/acceptance/bundle/resources/permissions/dashboards/create/script +++ b/acceptance/bundle/resources/permissions/dashboards/create/script @@ -15,18 +15,13 @@ rm out.requests.txt $CLI bundle debug plan > out.plan.$DATABRICKS_BUNDLE_ENGINE.json -print_requests() { - jq -c < out.requests.txt | jq 'select(.method != "GET" and (.path | contains("permissions")))' - rm out.requests.txt -} - rm out.requests.txt trace $CLI bundle deploy dashboard_id=$($CLI bundle summary --output json | jq -r '.resources.dashboards.foo.id') echo "$dashboard_id:DASHBOARD_ID" >> ACC_REPLS -print_requests > out.requests.deploy.$DATABRICKS_BUNDLE_ENGINE.json +print_requests.py //permissions > out.requests.deploy.$DATABRICKS_BUNDLE_ENGINE.json trace $CLI bundle destroy --auto-approve -print_requests > out.requests.destroy.$DATABRICKS_BUNDLE_ENGINE.json +print_requests.py //permissions > out.requests.destroy.$DATABRICKS_BUNDLE_ENGINE.json From d07abea36b3cb3f95efe34a5d7e0284387b49890 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 5 Nov 2025 18:36:16 +0100 Subject: [PATCH 5/8] remove all_test.go changs --- bundle/direct/dresources/all_test.go | 45 +--------------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index a199a20995..6e9384b3df 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -19,7 +19,6 @@ import ( "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/service/apps" "github.com/databricks/databricks-sdk-go/service/catalog" - "github.com/databricks/databricks-sdk-go/service/dashboards" "github.com/databricks/databricks-sdk-go/service/database" "github.com/databricks/databricks-sdk-go/service/iam" "github.com/databricks/databricks-sdk-go/service/jobs" @@ -98,16 +97,6 @@ var testConfig map[string]any = map[string]any{ }, }, }, - - "dashboards": &resources.Dashboard{ - DashboardConfig: resources.DashboardConfig{ - Dashboard: dashboards.Dashboard{ - DisplayName: "my_dashboard", - SerializedDashboard: `{"pages":[{"name":"page1","displayName":"Page 1"}]}`, - WarehouseId: "warehouse123", - }, - }, - }, } type prepareWorkspace func(client *databricks.WorkspaceClient) (any, error) @@ -263,27 +252,6 @@ var testDeps = map[string]prepareWorkspace{ }}, }, nil }, - - "dashboards.permissions": func(client *databricks.WorkspaceClient) (any, error) { - resp, err := client.Lakeview.Create(context.Background(), dashboards.CreateDashboardRequest{ - Dashboard: dashboards.Dashboard{ - DisplayName: "dashboard-permissions", - SerializedDashboard: `{"pages":[{"name":"page1","displayName":"Page 1"}]}`, - WarehouseId: "warehouse123", - }, - }) - if err != nil { - return nil, err - } - - return &PermissionsState{ - ObjectID: "/dashboards/" + resp.DashboardId, - Permissions: []iam.AccessControlRequest{{ - PermissionLevel: "CAN_MANAGE", - UserName: "user@example.com", - }}, - }, nil - }, } func TestAll(t *testing.T) { @@ -372,13 +340,7 @@ func testCRUD(t *testing.T, group string, adapter *Adapter, client *databricks.W var relevantChanges []structdiff.Change for _, change := range changes { fieldName := change.Path.String() - // Filter out fields that are expected to change between DoRefresh and DoUpdate - // - updated_at, update_time: timestamps that change on updates - // - etag: version field that changes on updates - // - path: computed field for dashboards - // - serialized_dashboard: test server adds pageType and formatting - if fieldName != "updated_at" && fieldName != "update_time" && fieldName != "etag" && - fieldName != "path" && fieldName != "serialized_dashboard" { + if fieldName != "updated_at" { relevantChanges = append(relevantChanges, change) } } @@ -412,11 +374,6 @@ func testCRUD(t *testing.T, group string, adapter *Adapter, client *databricks.W // We expect fields set explicitly to be preserved by testserver, which is true for all resources as of today. // If not true for your resource, add exception here: - // Dashboard serialized_dashboard is modified by the server (adds pageType, reorders keys, adds newline) - if path.String() == "serialized_dashboard" { - return - } - assert.Equal(t, val, remoteValue, "path=%q\nnewState=%s\nremappedState=%s", path.String(), jsonDump(newState), jsonDump(remappedState)) })) From fe2c3a7a216b3f521e9d9afa0c1e4f2cc6e6989d Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 5 Nov 2025 18:36:59 +0100 Subject: [PATCH 6/8] - --- bundle/direct/dresources/all_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index 6e9384b3df..230463ce74 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -373,7 +373,6 @@ func testCRUD(t *testing.T, group string, adapter *Adapter, client *databricks.W // t.Logf("Testing %s v=%#v, remoteValue=%#v", path.String(), val, remoteValue) // We expect fields set explicitly to be preserved by testserver, which is true for all resources as of today. // If not true for your resource, add exception here: - assert.Equal(t, val, remoteValue, "path=%q\nnewState=%s\nremappedState=%s", path.String(), jsonDump(newState), jsonDump(remappedState)) })) From 392b2ae0b7bc73797bf1793fb8bed7fc863b2458 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 5 Nov 2025 21:44:08 +0100 Subject: [PATCH 7/8] update tests --- acceptance/bundle/refschema/out.fields.txt | 7 +++++++ acceptance/bundle/resources/permissions/output.txt | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/acceptance/bundle/refschema/out.fields.txt b/acceptance/bundle/refschema/out.fields.txt index fd65a5a59f..7d16e50986 100644 --- a/acceptance/bundle/refschema/out.fields.txt +++ b/acceptance/bundle/refschema/out.fields.txt @@ -439,6 +439,13 @@ resources.dashboards.*.serialized_dashboard string ALL resources.dashboards.*.update_time string ALL resources.dashboards.*.url string INPUT resources.dashboards.*.warehouse_id string ALL +resources.dashboards.*.permissions.object_id string ALL +resources.dashboards.*.permissions.permissions []iam.AccessControlRequest ALL +resources.dashboards.*.permissions.permissions[*] iam.AccessControlRequest ALL +resources.dashboards.*.permissions.permissions[*].group_name string ALL +resources.dashboards.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL +resources.dashboards.*.permissions.permissions[*].service_principal_name string ALL +resources.dashboards.*.permissions.permissions[*].user_name string ALL resources.database_catalogs.*.create_database_if_not_exists bool ALL resources.database_catalogs.*.database_instance_name string ALL resources.database_catalogs.*.database_name string ALL diff --git a/acceptance/bundle/resources/permissions/output.txt b/acceptance/bundle/resources/permissions/output.txt index de0af0bbf7..5da10b6609 100644 --- a/acceptance/bundle/resources/permissions/output.txt +++ b/acceptance/bundle/resources/permissions/output.txt @@ -55,6 +55,18 @@ DIFF clusters/current_can_manage/out.requests.destroy.direct.json + "path": "/api/2.0/permissions/clusters/[UUID]" + } +] +MATCH dashboards/create/out.requests.deploy.direct.json +DIFF dashboards/create/out.requests.destroy.direct.json +--- dashboards/create/out.requests.destroy.direct.json ++++ dashboards/create/out.requests.destroy.terraform.json +@@ -1 +1,7 @@ +-[]+[ ++ { ++ "body": {}, ++ "method": "PUT", ++ "path": "/api/2.0/permissions/dashboards/[DASHBOARD_ID]" ++ } ++] MATCH database_instances/current_can_manage/out.requests.deploy.direct.json DIFF database_instances/current_can_manage/out.requests.destroy.direct.json --- database_instances/current_can_manage/out.requests.destroy.direct.json From 80eba8b942953e333d7717c81b5e3ebf5e143039 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 5 Nov 2025 22:09:07 +0100 Subject: [PATCH 8/8] fix test for dashboard permissions (TestAll) --- bundle/direct/dresources/all_test.go | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index 230463ce74..237184929f 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -19,6 +19,7 @@ import ( "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/service/apps" "github.com/databricks/databricks-sdk-go/service/catalog" + "github.com/databricks/databricks-sdk-go/service/dashboards" "github.com/databricks/databricks-sdk-go/service/database" "github.com/databricks/databricks-sdk-go/service/iam" "github.com/databricks/databricks-sdk-go/service/jobs" @@ -252,6 +253,36 @@ var testDeps = map[string]prepareWorkspace{ }}, }, nil }, + + "dashboards.permissions": func(client *databricks.WorkspaceClient) (any, error) { + ctx := context.Background() + parentPath := "/Workspace/Users/user@example.com" + + // Create parent directory if it doesn't exist + err := client.Workspace.MkdirsByPath(ctx, parentPath) + if err != nil { + return nil, err + } + + resp, err := client.Lakeview.Create(ctx, dashboards.CreateDashboardRequest{ + Dashboard: dashboards.Dashboard{ + DisplayName: "dashboard-permissions", + ParentPath: parentPath, + SerializedDashboard: `{"pages":[{"name":"page1","displayName":"Page 1"}]}`, + }, + }) + if err != nil { + return nil, err + } + + return &PermissionsState{ + ObjectID: "/dashboards/" + resp.DashboardId, + Permissions: []iam.AccessControlRequest{{ + PermissionLevel: "CAN_MANAGE", + UserName: "user@example.com", + }}, + }, nil + }, } func TestAll(t *testing.T) {