diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index d5a0e34975..c5e0e96e50 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -5,6 +5,7 @@ ### CLI ### Bundles +* direct: Fix permissions state path to match input config schema ([#4703](https://github.com/databricks/cli/pull/4703)) ### Dependency updates diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index b17ab98806..c32e4f7b3b 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -209,6 +209,12 @@ func testAccept(t *testing.T, inprocessMode bool, singleTest string) int { t.Setenv("CLI", execPath) repls.SetPath(execPath, "[CLI]") + if !inprocessMode { + cli293Path := DownloadCLI(t, buildDir, "0.293.0") + t.Setenv("CLI_293", cli293Path) + repls.SetPath(cli293Path, "[CLI_293]") + } + paths := []string{ // Make helper scripts available filepath.Join(cwd, "bin"), diff --git a/acceptance/bundle/apps/job_permissions/out.test.toml b/acceptance/bundle/apps/job_permissions/out.test.toml index a9f28de48a..01ed6822af 100644 --- a/acceptance/bundle/apps/job_permissions/out.test.toml +++ b/acceptance/bundle/apps/job_permissions/out.test.toml @@ -2,4 +2,4 @@ Local = true Cloud = true [EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["terraform"] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/apps/job_permissions/test.toml b/acceptance/bundle/apps/job_permissions/test.toml index 4935cf6732..5ce04e476b 100644 --- a/acceptance/bundle/apps/job_permissions/test.toml +++ b/acceptance/bundle/apps/job_permissions/test.toml @@ -1,11 +1,5 @@ -# Direct engine error: cannot plan resources.jobs.my_job.permissions: cannot update -# [0].service_principal_name: failed to navigate to parent [0]: [0]: cannot index struct. -# This is a bug in structaccess.Set() where it fails to index into a struct when -# setting permissions with service_principal_name. -# See https://github.com/databricks/cli/pull/4644 -Badness = "Direct engine fails to plan permissions with service_principal_name on jobs" Cloud = true RecordRequests = false [EnvMatrix] -DATABRICKS_BUNDLE_ENGINE = ["terraform"] +DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/deployment/bind/dashboard/recreation/out.state_after_bind.direct.json b/acceptance/bundle/deployment/bind/dashboard/recreation/out.state_after_bind.direct.json index f839adb1cd..771dd3f908 100644 --- a/acceptance/bundle/deployment/bind/dashboard/recreation/out.state_after_bind.direct.json +++ b/acceptance/bundle/deployment/bind/dashboard/recreation/out.state_after_bind.direct.json @@ -1,5 +1,5 @@ { - "state_version": 1, + "state_version": 2, "cli_version": "[DEV_VERSION]", "lineage": "[UUID]", "serial": 2, diff --git a/acceptance/bundle/invariant/configs/job_cross_resource_ref.yml.tmpl b/acceptance/bundle/invariant/configs/job_cross_resource_ref.yml.tmpl new file mode 100644 index 0000000000..c3c06a79af --- /dev/null +++ b/acceptance/bundle/invariant/configs/job_cross_resource_ref.yml.tmpl @@ -0,0 +1,27 @@ +bundle: + name: test-bundle-$UNIQUE_NAME + +resources: + jobs: + # job_src defines permissions and a tag value used as references by other resources + job_src: + name: test-job-src-$UNIQUE_NAME + tags: + perm_group: users + permissions: + - level: CAN_VIEW + group_name: users + + # job_perm_ref uses permission fields from job_src as its permission values + job_perm_ref: + name: test-job-perm-ref-$UNIQUE_NAME + permissions: + - level: ${resources.jobs.job_src.permissions[0].level} + group_name: ${resources.jobs.job_src.permissions[0].group_name} + + # job_tag_ref uses a job tag from job_src as a permission group_name + job_tag_ref: + name: test-job-tag-ref-$UNIQUE_NAME + permissions: + - level: CAN_VIEW + group_name: ${resources.jobs.job_src.tags.perm_group} diff --git a/acceptance/bundle/invariant/configs/job_permission_ref.yml.tmpl b/acceptance/bundle/invariant/configs/job_permission_ref.yml.tmpl new file mode 100644 index 0000000000..3eec8c8dc3 --- /dev/null +++ b/acceptance/bundle/invariant/configs/job_permission_ref.yml.tmpl @@ -0,0 +1,21 @@ +bundle: + name: test-bundle-$UNIQUE_NAME + +resources: + jobs: + job_b: + name: test-job-b-$UNIQUE_NAME + permissions: + - level: CAN_VIEW + group_name: users + - level: CAN_MANAGE + group_name: admins + + job_a: + name: test-job-a-$UNIQUE_NAME + permissions: + # Reference level and group_name from job_b by index + - level: ${resources.jobs.job_b.permissions[0].level} + group_name: ${resources.jobs.job_b.permissions[0].group_name} + - level: ${resources.jobs.job_b.permissions[1].level} + group_name: ${resources.jobs.job_b.permissions[1].group_name} diff --git a/acceptance/bundle/invariant/continue_293/out.test.toml b/acceptance/bundle/invariant/continue_293/out.test.toml new file mode 100644 index 0000000000..a31103a9c8 --- /dev/null +++ b/acceptance/bundle/invariant/continue_293/out.test.toml @@ -0,0 +1,7 @@ +Local = true +Cloud = false +RequiresUnityCatalog = true + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["direct"] + INPUT_CONFIG = ["alert.yml.tmpl", "app.yml.tmpl", "catalog.yml.tmpl", "cluster.yml.tmpl", "dashboard.yml.tmpl", "database_catalog.yml.tmpl", "database_instance.yml.tmpl", "experiment.yml.tmpl", "external_location.yml.tmpl", "job.yml.tmpl", "job_pydabs_10_tasks.yml.tmpl", "job_pydabs_1000_tasks.yml.tmpl", "job_cross_resource_ref.yml.tmpl", "job_permission_ref.yml.tmpl", "job_with_task.yml.tmpl", "model.yml.tmpl", "model_serving_endpoint.yml.tmpl", "pipeline.yml.tmpl", "postgres_branch.yml.tmpl", "postgres_endpoint.yml.tmpl", "postgres_project.yml.tmpl", "registered_model.yml.tmpl", "schema.yml.tmpl", "secret_scope.yml.tmpl", "synced_database_table.yml.tmpl", "volume.yml.tmpl"] diff --git a/acceptance/bundle/invariant/continue_293/output.txt b/acceptance/bundle/invariant/continue_293/output.txt new file mode 100644 index 0000000000..7a28cb73a5 --- /dev/null +++ b/acceptance/bundle/invariant/continue_293/output.txt @@ -0,0 +1 @@ +INPUT_CONFIG_OK diff --git a/acceptance/bundle/invariant/continue_293/script b/acceptance/bundle/invariant/continue_293/script new file mode 100644 index 0000000000..6f3d23c2bb --- /dev/null +++ b/acceptance/bundle/invariant/continue_293/script @@ -0,0 +1,40 @@ +# Invariant to test: current CLI can deploy on top of state produced by v0.293.0 + +cp -r "$TESTDIR/../data/." . &> LOG.cp + +INIT_SCRIPT="$TESTDIR/../configs/$INPUT_CONFIG-init.sh" +if [ -f "$INIT_SCRIPT" ]; then + source "$INIT_SCRIPT" &> LOG.init +fi + +envsubst < "$TESTDIR/../configs/$INPUT_CONFIG" > databricks.yml + +cleanup() { + trace $CLI bundle destroy --auto-approve &> LOG.destroy + cat LOG.destroy | contains.py '!panic' '!internal error' > /dev/null + + CLEANUP_SCRIPT="$TESTDIR/../configs/$INPUT_CONFIG-cleanup.sh" + if [ -f "$CLEANUP_SCRIPT" ]; then + source "$CLEANUP_SCRIPT" &> LOG.cleanup + fi +} + +trap cleanup EXIT + +# Deploy with old CLI to produce v0.293.0 state +trace $CLI_293 bundle deploy &> LOG.deploy.293 +cat LOG.deploy.293 | contains.py '!panic' '!internal error' > /dev/null + +echo INPUT_CONFIG_OK + +# Deploy with current CLI on top of old state +trace $CLI bundle deploy &> LOG.deploy +cat LOG.deploy | contains.py '!panic' '!internal error' > /dev/null + +# Verify no drift after current CLI deploy +$CLI bundle plan -o json > LOG.planjson 2>LOG.planjson.err +cat LOG.planjson.err | contains.py '!panic' '!internal error' > /dev/null +verify_no_drift.py LOG.planjson + +$CLI bundle plan 2>LOG.plan.err | contains.py '!panic' '!internal error' 'Plan: 0 to add, 0 to change, 0 to delete' > LOG.plan +cat LOG.plan.err | contains.py '!panic' '!internal error' > /dev/null diff --git a/acceptance/bundle/invariant/continue_293/test.toml b/acceptance/bundle/invariant/continue_293/test.toml new file mode 100644 index 0000000000..a1d5170bef --- /dev/null +++ b/acceptance/bundle/invariant/continue_293/test.toml @@ -0,0 +1,5 @@ +Cloud = false + +# Cross-resource permission references don't work in terraform mode. +EnvMatrixExclude.no_permission_ref = ["INPUT_CONFIG=job_permission_ref.yml.tmpl"] +EnvMatrixExclude.no_cross_resource_ref = ["INPUT_CONFIG=job_cross_resource_ref.yml.tmpl"] diff --git a/acceptance/bundle/invariant/migrate/out.test.toml b/acceptance/bundle/invariant/migrate/out.test.toml index d2d74b0776..8c2993e924 100644 --- a/acceptance/bundle/invariant/migrate/out.test.toml +++ b/acceptance/bundle/invariant/migrate/out.test.toml @@ -4,4 +4,4 @@ RequiresUnityCatalog = true [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["direct"] - INPUT_CONFIG = ["alert.yml.tmpl", "app.yml.tmpl", "catalog.yml.tmpl", "cluster.yml.tmpl", "dashboard.yml.tmpl", "database_catalog.yml.tmpl", "database_instance.yml.tmpl", "experiment.yml.tmpl", "external_location.yml.tmpl", "job.yml.tmpl", "job_pydabs_10_tasks.yml.tmpl", "job_pydabs_1000_tasks.yml.tmpl", "job_with_task.yml.tmpl", "model.yml.tmpl", "model_serving_endpoint.yml.tmpl", "pipeline.yml.tmpl", "postgres_branch.yml.tmpl", "postgres_endpoint.yml.tmpl", "postgres_project.yml.tmpl", "registered_model.yml.tmpl", "schema.yml.tmpl", "secret_scope.yml.tmpl", "synced_database_table.yml.tmpl", "volume.yml.tmpl"] + INPUT_CONFIG = ["alert.yml.tmpl", "app.yml.tmpl", "catalog.yml.tmpl", "cluster.yml.tmpl", "dashboard.yml.tmpl", "database_catalog.yml.tmpl", "database_instance.yml.tmpl", "experiment.yml.tmpl", "external_location.yml.tmpl", "job.yml.tmpl", "job_pydabs_10_tasks.yml.tmpl", "job_pydabs_1000_tasks.yml.tmpl", "job_cross_resource_ref.yml.tmpl", "job_permission_ref.yml.tmpl", "job_with_task.yml.tmpl", "model.yml.tmpl", "model_serving_endpoint.yml.tmpl", "pipeline.yml.tmpl", "postgres_branch.yml.tmpl", "postgres_endpoint.yml.tmpl", "postgres_project.yml.tmpl", "registered_model.yml.tmpl", "schema.yml.tmpl", "secret_scope.yml.tmpl", "synced_database_table.yml.tmpl", "volume.yml.tmpl"] diff --git a/acceptance/bundle/invariant/migrate/script b/acceptance/bundle/invariant/migrate/script index 3f6cfed948..d02200cb53 100644 --- a/acceptance/bundle/invariant/migrate/script +++ b/acceptance/bundle/invariant/migrate/script @@ -38,6 +38,6 @@ trace $CLI bundle deployment migrate &> LOG.migrate cat LOG.migrate | contains.py '!panic:' '!internal error' > /dev/null -$CLI bundle plan -o json &> plan.json -cat plan.json | contains.py '!panic:' '!internal error' > /dev/null +$CLI bundle plan -o json > plan.json 2>plan.json.err +cat plan.json.err | contains.py '!panic:' '!internal error' > /dev/null verify_no_drift.py plan.json diff --git a/acceptance/bundle/invariant/migrate/test.toml b/acceptance/bundle/invariant/migrate/test.toml index 0825572f1d..781987f7ca 100644 --- a/acceptance/bundle/invariant/migrate/test.toml +++ b/acceptance/bundle/invariant/migrate/test.toml @@ -4,3 +4,12 @@ EnvMatrixExclude.no_external_location = ["INPUT_CONFIG=external_location.yml.tmp # Unexpected action='create' for resources.secret_scopes.foo.permissions EnvMatrixExclude.no_secret_scope = ["INPUT_CONFIG=secret_scope.yml.tmpl"] + +# Cross-resource permission references (e.g. ${resources.jobs.job_b.permissions[0].level}) +# don't work in terraform mode: the terraform interpolator converts the path to +# ${databricks_job.job_b.permissions[0].level}, but Terraform's databricks_job resource +# does not expose permissions as output attributes (permissions are a separate +# databricks_permissions resource in terraform), so the literal unresolved string +# ends up as the permission level value. +EnvMatrixExclude.no_permission_ref = ["INPUT_CONFIG=job_permission_ref.yml.tmpl"] +EnvMatrixExclude.no_cross_resource_ref = ["INPUT_CONFIG=job_cross_resource_ref.yml.tmpl"] diff --git a/acceptance/bundle/invariant/no_drift/out.test.toml b/acceptance/bundle/invariant/no_drift/out.test.toml index d2d74b0776..8c2993e924 100644 --- a/acceptance/bundle/invariant/no_drift/out.test.toml +++ b/acceptance/bundle/invariant/no_drift/out.test.toml @@ -4,4 +4,4 @@ RequiresUnityCatalog = true [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["direct"] - INPUT_CONFIG = ["alert.yml.tmpl", "app.yml.tmpl", "catalog.yml.tmpl", "cluster.yml.tmpl", "dashboard.yml.tmpl", "database_catalog.yml.tmpl", "database_instance.yml.tmpl", "experiment.yml.tmpl", "external_location.yml.tmpl", "job.yml.tmpl", "job_pydabs_10_tasks.yml.tmpl", "job_pydabs_1000_tasks.yml.tmpl", "job_with_task.yml.tmpl", "model.yml.tmpl", "model_serving_endpoint.yml.tmpl", "pipeline.yml.tmpl", "postgres_branch.yml.tmpl", "postgres_endpoint.yml.tmpl", "postgres_project.yml.tmpl", "registered_model.yml.tmpl", "schema.yml.tmpl", "secret_scope.yml.tmpl", "synced_database_table.yml.tmpl", "volume.yml.tmpl"] + INPUT_CONFIG = ["alert.yml.tmpl", "app.yml.tmpl", "catalog.yml.tmpl", "cluster.yml.tmpl", "dashboard.yml.tmpl", "database_catalog.yml.tmpl", "database_instance.yml.tmpl", "experiment.yml.tmpl", "external_location.yml.tmpl", "job.yml.tmpl", "job_pydabs_10_tasks.yml.tmpl", "job_pydabs_1000_tasks.yml.tmpl", "job_cross_resource_ref.yml.tmpl", "job_permission_ref.yml.tmpl", "job_with_task.yml.tmpl", "model.yml.tmpl", "model_serving_endpoint.yml.tmpl", "pipeline.yml.tmpl", "postgres_branch.yml.tmpl", "postgres_endpoint.yml.tmpl", "postgres_project.yml.tmpl", "registered_model.yml.tmpl", "schema.yml.tmpl", "secret_scope.yml.tmpl", "synced_database_table.yml.tmpl", "volume.yml.tmpl"] diff --git a/acceptance/bundle/invariant/no_drift/script b/acceptance/bundle/invariant/no_drift/script index 6d11ed7b1c..0473672e16 100644 --- a/acceptance/bundle/invariant/no_drift/script +++ b/acceptance/bundle/invariant/no_drift/script @@ -41,8 +41,9 @@ echo INPUT_CONFIG_OK # Check both text and JSON plan for no changes # Note, expect that there maybe more than one resource unchanged -$CLI bundle plan -o json &> LOG.planjson -cat LOG.planjson | contains.py '!panic' '!internal error' > /dev/null +$CLI bundle plan -o json > LOG.planjson 2>LOG.planjson.err +cat LOG.planjson.err | contains.py '!panic' '!internal error' > /dev/null verify_no_drift.py LOG.planjson -$CLI bundle plan | contains.py '!panic' '!internal error' 'Plan: 0 to add, 0 to change, 0 to delete' > LOG.plan +$CLI bundle plan 2>LOG.plan.err | contains.py '!panic' '!internal error' 'Plan: 0 to add, 0 to change, 0 to delete' > LOG.plan +cat LOG.plan.err | contains.py '!panic' '!internal error' > /dev/null diff --git a/acceptance/bundle/invariant/test.toml b/acceptance/bundle/invariant/test.toml index 44a44e6808..e3e4e644fe 100644 --- a/acceptance/bundle/invariant/test.toml +++ b/acceptance/bundle/invariant/test.toml @@ -10,6 +10,7 @@ Ignore = [ "plan.json", "*.py", "*.json", + "*.err", "app", ] @@ -32,6 +33,8 @@ EnvMatrix.INPUT_CONFIG = [ "job.yml.tmpl", "job_pydabs_10_tasks.yml.tmpl", "job_pydabs_1000_tasks.yml.tmpl", + "job_cross_resource_ref.yml.tmpl", + "job_permission_ref.yml.tmpl", "job_with_task.yml.tmpl", "model.yml.tmpl", "model_serving_endpoint.yml.tmpl", diff --git a/acceptance/bundle/migrate/basic/out.new_state.json b/acceptance/bundle/migrate/basic/out.new_state.json index de537f2f4b..f6bdf06f62 100644 --- a/acceptance/bundle/migrate/basic/out.new_state.json +++ b/acceptance/bundle/migrate/basic/out.new_state.json @@ -1,5 +1,5 @@ { - "state_version": 1, + "state_version": 2, "cli_version": "[DEV_VERSION]", "lineage": "[UUID]", "serial": 6, diff --git a/acceptance/bundle/migrate/dashboards/out.new_state.json b/acceptance/bundle/migrate/dashboards/out.new_state.json index 8a275248f8..695d14602a 100644 --- a/acceptance/bundle/migrate/dashboards/out.new_state.json +++ b/acceptance/bundle/migrate/dashboards/out.new_state.json @@ -1,5 +1,5 @@ { - "state_version": 1, + "state_version": 2, "cli_version": "[DEV_VERSION]", "lineage": "[UUID]", "serial": 3, diff --git a/acceptance/bundle/migrate/default-python/out.state_after_migration.json b/acceptance/bundle/migrate/default-python/out.state_after_migration.json index c29e8fbd7c..029649aae3 100644 --- a/acceptance/bundle/migrate/default-python/out.state_after_migration.json +++ b/acceptance/bundle/migrate/default-python/out.state_after_migration.json @@ -1,5 +1,5 @@ { - "state_version": 1, + "state_version": 2, "cli_version": "[DEV_VERSION]", "lineage": "[UUID]", "serial": 5, diff --git a/acceptance/bundle/migrate/grants/out.new_state.json b/acceptance/bundle/migrate/grants/out.new_state.json index 8a0116f88a..2046d78c9d 100644 --- a/acceptance/bundle/migrate/grants/out.new_state.json +++ b/acceptance/bundle/migrate/grants/out.new_state.json @@ -1,5 +1,5 @@ { - "state_version": 1, + "state_version": 2, "cli_version": "[DEV_VERSION]", "lineage": "[UUID]", "serial": 9, diff --git a/acceptance/bundle/migrate/permissions/out.new_state.json b/acceptance/bundle/migrate/permissions/out.new_state.json index 9bb913044f..804e77c918 100644 --- a/acceptance/bundle/migrate/permissions/out.new_state.json +++ b/acceptance/bundle/migrate/permissions/out.new_state.json @@ -1,5 +1,5 @@ { - "state_version": 1, + "state_version": 2, "cli_version": "[DEV_VERSION]", "lineage": "[UUID]", "serial": 7, @@ -32,13 +32,13 @@ "__id__": "/jobs/[NUMID]", "state": { "object_id": "/jobs/[NUMID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] @@ -73,13 +73,13 @@ "__id__": "/pipelines/[UUID]", "state": { "object_id": "/pipelines/[UUID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "manager@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/migrate/runas/out.new_state.json b/acceptance/bundle/migrate/runas/out.new_state.json index 017f90d468..4861c4d68a 100644 --- a/acceptance/bundle/migrate/runas/out.new_state.json +++ b/acceptance/bundle/migrate/runas/out.new_state.json @@ -1,5 +1,5 @@ { - "state_version": 1, + "state_version": 2, "cli_version": "[DEV_VERSION]", "lineage": "[UUID]", "serial": 5, @@ -33,13 +33,13 @@ "__id__": "/pipelines/[UUID]", "state": { "object_id": "/pipelines/[UUID]", - "permissions": [ + "_": [ { - "group_name": "users", - "permission_level": "CAN_VIEW" + "level": "CAN_VIEW", + "group_name": "users" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/migrate/runas/out.plan.json b/acceptance/bundle/migrate/runas/out.plan.json index bdab0ae3bb..4c2276f3ca 100644 --- a/acceptance/bundle/migrate/runas/out.plan.json +++ b/acceptance/bundle/migrate/runas/out.plan.json @@ -54,13 +54,13 @@ "action": "skip", "remote_state": { "object_id": "/pipelines/[UUID]", - "permissions": [ + "_": [ { - "group_name": "users", - "permission_level": "CAN_VIEW" + "level": "CAN_VIEW", + "group_name": "users" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/refschema/out.fields.txt b/acceptance/bundle/refschema/out.fields.txt index 83fff3d0c3..1ca63c3941 100644 --- a/acceptance/bundle/refschema/out.fields.txt +++ b/acceptance/bundle/refschema/out.fields.txt @@ -57,12 +57,11 @@ resources.alerts.*.update_time string ALL resources.alerts.*.url string INPUT resources.alerts.*.warehouse_id string ALL resources.alerts.*.permissions.object_id string ALL -resources.alerts.*.permissions.permissions []iam.AccessControlRequest ALL -resources.alerts.*.permissions.permissions[*] iam.AccessControlRequest ALL -resources.alerts.*.permissions.permissions[*].group_name string ALL -resources.alerts.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL -resources.alerts.*.permissions.permissions[*].service_principal_name string ALL -resources.alerts.*.permissions.permissions[*].user_name string ALL +resources.alerts.*.permissions[*] dresources.StatePermission ALL +resources.alerts.*.permissions[*].group_name string ALL +resources.alerts.*.permissions[*].level iam.PermissionLevel ALL +resources.alerts.*.permissions[*].service_principal_name string ALL +resources.alerts.*.permissions[*].user_name string ALL resources.apps.*.active_deployment *apps.AppDeployment ALL resources.apps.*.active_deployment.command []string ALL resources.apps.*.active_deployment.command[*] string ALL @@ -225,12 +224,11 @@ resources.apps.*.usage_policy_id string ALL resources.apps.*.user_api_scopes []string ALL resources.apps.*.user_api_scopes[*] string ALL resources.apps.*.permissions.object_id string ALL -resources.apps.*.permissions.permissions []iam.AccessControlRequest ALL -resources.apps.*.permissions.permissions[*] iam.AccessControlRequest ALL -resources.apps.*.permissions.permissions[*].group_name string ALL -resources.apps.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL -resources.apps.*.permissions.permissions[*].service_principal_name string ALL -resources.apps.*.permissions.permissions[*].user_name string ALL +resources.apps.*.permissions[*] dresources.StatePermission ALL +resources.apps.*.permissions[*].group_name string ALL +resources.apps.*.permissions[*].level iam.PermissionLevel ALL +resources.apps.*.permissions[*].service_principal_name string ALL +resources.apps.*.permissions[*].user_name string ALL resources.catalogs.*.browse_only bool REMOTE resources.catalogs.*.catalog_type catalog.CatalogType REMOTE resources.catalogs.*.comment string ALL @@ -546,12 +544,11 @@ resources.clusters.*.workload_type.clients compute.ClientsTypes ALL resources.clusters.*.workload_type.clients.jobs bool ALL resources.clusters.*.workload_type.clients.notebooks bool ALL resources.clusters.*.permissions.object_id string ALL -resources.clusters.*.permissions.permissions []iam.AccessControlRequest ALL -resources.clusters.*.permissions.permissions[*] iam.AccessControlRequest ALL -resources.clusters.*.permissions.permissions[*].group_name string ALL -resources.clusters.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL -resources.clusters.*.permissions.permissions[*].service_principal_name string ALL -resources.clusters.*.permissions.permissions[*].user_name string ALL +resources.clusters.*.permissions[*] dresources.StatePermission ALL +resources.clusters.*.permissions[*].group_name string ALL +resources.clusters.*.permissions[*].level iam.PermissionLevel ALL +resources.clusters.*.permissions[*].service_principal_name string ALL +resources.clusters.*.permissions[*].user_name string ALL resources.dashboards.*.create_time string ALL resources.dashboards.*.dashboard_id string ALL resources.dashboards.*.dataset_catalog string ALL @@ -579,12 +576,11 @@ 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.dashboards.*.permissions[*] dresources.StatePermission ALL +resources.dashboards.*.permissions[*].group_name string ALL +resources.dashboards.*.permissions[*].level iam.PermissionLevel ALL +resources.dashboards.*.permissions[*].service_principal_name string ALL +resources.dashboards.*.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 @@ -650,12 +646,11 @@ resources.database_instances.*.uid string ALL resources.database_instances.*.url string INPUT resources.database_instances.*.usage_policy_id string ALL resources.database_instances.*.permissions.object_id string ALL -resources.database_instances.*.permissions.permissions []iam.AccessControlRequest ALL -resources.database_instances.*.permissions.permissions[*] iam.AccessControlRequest ALL -resources.database_instances.*.permissions.permissions[*].group_name string ALL -resources.database_instances.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL -resources.database_instances.*.permissions.permissions[*].service_principal_name string ALL -resources.database_instances.*.permissions.permissions[*].user_name string ALL +resources.database_instances.*.permissions[*] dresources.StatePermission ALL +resources.database_instances.*.permissions[*].group_name string ALL +resources.database_instances.*.permissions[*].level iam.PermissionLevel ALL +resources.database_instances.*.permissions[*].service_principal_name string ALL +resources.database_instances.*.permissions[*].user_name string ALL resources.experiments.*.artifact_location string ALL resources.experiments.*.creation_time int64 REMOTE resources.experiments.*.experiment_id string REMOTE @@ -678,12 +673,11 @@ resources.experiments.*.tags[*].key string ALL resources.experiments.*.tags[*].value string ALL resources.experiments.*.url string INPUT resources.experiments.*.permissions.object_id string ALL -resources.experiments.*.permissions.permissions []iam.AccessControlRequest ALL -resources.experiments.*.permissions.permissions[*] iam.AccessControlRequest ALL -resources.experiments.*.permissions.permissions[*].group_name string ALL -resources.experiments.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL -resources.experiments.*.permissions.permissions[*].service_principal_name string ALL -resources.experiments.*.permissions.permissions[*].user_name string ALL +resources.experiments.*.permissions[*] dresources.StatePermission ALL +resources.experiments.*.permissions[*].group_name string ALL +resources.experiments.*.permissions[*].level iam.PermissionLevel ALL +resources.experiments.*.permissions[*].service_principal_name string ALL +resources.experiments.*.permissions[*].user_name string ALL resources.external_locations.*.browse_only bool REMOTE resources.external_locations.*.comment string ALL resources.external_locations.*.created_at int64 REMOTE @@ -1649,12 +1643,11 @@ resources.jobs.*.webhook_notifications.on_success []jobs.Webhook ALL resources.jobs.*.webhook_notifications.on_success[*] jobs.Webhook ALL resources.jobs.*.webhook_notifications.on_success[*].id string ALL resources.jobs.*.permissions.object_id string ALL -resources.jobs.*.permissions.permissions []iam.AccessControlRequest ALL -resources.jobs.*.permissions.permissions[*] iam.AccessControlRequest ALL -resources.jobs.*.permissions.permissions[*].group_name string ALL -resources.jobs.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL -resources.jobs.*.permissions.permissions[*].service_principal_name string ALL -resources.jobs.*.permissions.permissions[*].user_name string ALL +resources.jobs.*.permissions[*] dresources.StatePermission ALL +resources.jobs.*.permissions[*].group_name string ALL +resources.jobs.*.permissions[*].level iam.PermissionLevel ALL +resources.jobs.*.permissions[*].service_principal_name string ALL +resources.jobs.*.permissions[*].user_name string ALL resources.model_serving_endpoints.*.ai_gateway *serving.AiGatewayConfig INPUT STATE resources.model_serving_endpoints.*.ai_gateway.fallback_config *serving.FallbackConfig INPUT STATE resources.model_serving_endpoints.*.ai_gateway.fallback_config.enabled bool INPUT STATE @@ -2124,12 +2117,11 @@ resources.model_serving_endpoints.*.tags[*].key string INPUT STATE resources.model_serving_endpoints.*.tags[*].value string INPUT STATE resources.model_serving_endpoints.*.url string INPUT resources.model_serving_endpoints.*.permissions.object_id string ALL -resources.model_serving_endpoints.*.permissions.permissions []iam.AccessControlRequest ALL -resources.model_serving_endpoints.*.permissions.permissions[*] iam.AccessControlRequest ALL -resources.model_serving_endpoints.*.permissions.permissions[*].group_name string ALL -resources.model_serving_endpoints.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL -resources.model_serving_endpoints.*.permissions.permissions[*].service_principal_name string ALL -resources.model_serving_endpoints.*.permissions.permissions[*].user_name string ALL +resources.model_serving_endpoints.*.permissions[*] dresources.StatePermission ALL +resources.model_serving_endpoints.*.permissions[*].group_name string ALL +resources.model_serving_endpoints.*.permissions[*].level iam.PermissionLevel ALL +resources.model_serving_endpoints.*.permissions[*].service_principal_name string ALL +resources.model_serving_endpoints.*.permissions[*].user_name string ALL resources.models.*.creation_timestamp int64 REMOTE resources.models.*.description string ALL resources.models.*.id string INPUT REMOTE @@ -2170,12 +2162,11 @@ resources.models.*.tags[*].value string ALL resources.models.*.url string INPUT resources.models.*.user_id string REMOTE resources.models.*.permissions.object_id string ALL -resources.models.*.permissions.permissions []iam.AccessControlRequest ALL -resources.models.*.permissions.permissions[*] iam.AccessControlRequest ALL -resources.models.*.permissions.permissions[*].group_name string ALL -resources.models.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL -resources.models.*.permissions.permissions[*].service_principal_name string ALL -resources.models.*.permissions.permissions[*].user_name string ALL +resources.models.*.permissions[*] dresources.StatePermission ALL +resources.models.*.permissions[*].group_name string ALL +resources.models.*.permissions[*].level iam.PermissionLevel ALL +resources.models.*.permissions[*].service_principal_name string ALL +resources.models.*.permissions[*].user_name string ALL resources.pipelines.*.allow_duplicate_names bool ALL resources.pipelines.*.budget_policy_id string ALL resources.pipelines.*.catalog string ALL @@ -2515,12 +2506,11 @@ resources.pipelines.*.trigger.manual *pipelines.ManualTrigger ALL resources.pipelines.*.url string INPUT resources.pipelines.*.usage_policy_id string ALL resources.pipelines.*.permissions.object_id string ALL -resources.pipelines.*.permissions.permissions []iam.AccessControlRequest ALL -resources.pipelines.*.permissions.permissions[*] iam.AccessControlRequest ALL -resources.pipelines.*.permissions.permissions[*].group_name string ALL -resources.pipelines.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL -resources.pipelines.*.permissions.permissions[*].service_principal_name string ALL -resources.pipelines.*.permissions.permissions[*].user_name string ALL +resources.pipelines.*.permissions[*] dresources.StatePermission ALL +resources.pipelines.*.permissions[*].group_name string ALL +resources.pipelines.*.permissions[*].level iam.PermissionLevel ALL +resources.pipelines.*.permissions[*].service_principal_name string ALL +resources.pipelines.*.permissions[*].user_name string ALL resources.postgres_branches.*.branch_id string INPUT STATE resources.postgres_branches.*.create_time *time.Time REMOTE resources.postgres_branches.*.expire_time *time.Time INPUT STATE @@ -2689,12 +2679,11 @@ resources.postgres_projects.*.uid string REMOTE resources.postgres_projects.*.update_time *time.Time REMOTE resources.postgres_projects.*.url string INPUT resources.postgres_projects.*.permissions.object_id string ALL -resources.postgres_projects.*.permissions.permissions []iam.AccessControlRequest ALL -resources.postgres_projects.*.permissions.permissions[*] iam.AccessControlRequest ALL -resources.postgres_projects.*.permissions.permissions[*].group_name string ALL -resources.postgres_projects.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL -resources.postgres_projects.*.permissions.permissions[*].service_principal_name string ALL -resources.postgres_projects.*.permissions.permissions[*].user_name string ALL +resources.postgres_projects.*.permissions[*] dresources.StatePermission ALL +resources.postgres_projects.*.permissions[*].group_name string ALL +resources.postgres_projects.*.permissions[*].level iam.PermissionLevel ALL +resources.postgres_projects.*.permissions[*].service_principal_name string ALL +resources.postgres_projects.*.permissions[*].user_name string ALL resources.quality_monitors.*.assets_dir string ALL resources.quality_monitors.*.baseline_table_name string ALL resources.quality_monitors.*.custom_metrics []catalog.MonitorMetric ALL @@ -2904,12 +2893,11 @@ resources.sql_warehouses.*.url string INPUT resources.sql_warehouses.*.warehouse_type sql.CreateWarehouseRequestWarehouseType INPUT STATE resources.sql_warehouses.*.warehouse_type sql.GetWarehouseResponseWarehouseType REMOTE resources.sql_warehouses.*.permissions.object_id string ALL -resources.sql_warehouses.*.permissions.permissions []iam.AccessControlRequest ALL -resources.sql_warehouses.*.permissions.permissions[*] iam.AccessControlRequest ALL -resources.sql_warehouses.*.permissions.permissions[*].group_name string ALL -resources.sql_warehouses.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL -resources.sql_warehouses.*.permissions.permissions[*].service_principal_name string ALL -resources.sql_warehouses.*.permissions.permissions[*].user_name string ALL +resources.sql_warehouses.*.permissions[*] dresources.StatePermission ALL +resources.sql_warehouses.*.permissions[*].group_name string ALL +resources.sql_warehouses.*.permissions[*].level iam.PermissionLevel ALL +resources.sql_warehouses.*.permissions[*].service_principal_name string ALL +resources.sql_warehouses.*.permissions[*].user_name string ALL resources.synced_database_tables.*.data_synchronization_status *database.SyncedTableStatus ALL resources.synced_database_tables.*.data_synchronization_status.continuous_update_status *database.SyncedTableContinuousUpdateStatus ALL resources.synced_database_tables.*.data_synchronization_status.continuous_update_status.initial_pipeline_sync_progress *database.SyncedTablePipelineProgress ALL diff --git a/acceptance/bundle/resource_deps/permission_ref/databricks.yml b/acceptance/bundle/resource_deps/permission_ref/databricks.yml new file mode 100644 index 0000000000..4ef5f377a2 --- /dev/null +++ b/acceptance/bundle/resource_deps/permission_ref/databricks.yml @@ -0,0 +1,24 @@ +bundle: + name: test-bundle + +resources: + jobs: + # job_b has permissions that job_a references + job_b: + name: job B + permissions: + - level: CAN_VIEW + group_name: viewers + - level: CAN_MANAGE + group_name: admins + + # job_a references job_b's permission levels + job_a: + name: job A + permissions: + # Reference by integer index + - level: ${resources.jobs.job_b.permissions[0].level} + group_name: team-alpha + # Reference by integer index (second entry) + - level: ${resources.jobs.job_b.permissions[1].level} + group_name: team-beta diff --git a/acceptance/bundle/resource_deps/permission_ref/out.plan_create.direct.json b/acceptance/bundle/resource_deps/permission_ref/out.plan_create.direct.json new file mode 100644 index 0000000000..df5e26a203 --- /dev/null +++ b/acceptance/bundle/resource_deps/permission_ref/out.plan_create.direct.json @@ -0,0 +1,112 @@ +{ + "plan_version": 2, + "cli_version": "[DEV_VERSION]", + "plan": { + "resources.jobs.job_a": { + "action": "create", + "new_state": { + "value": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "job A", + "queue": { + "enabled": true + } + } + } + }, + "resources.jobs.job_a.permissions": { + "depends_on": [ + { + "node": "resources.jobs.job_a", + "label": "${resources.jobs.job_a.id}" + }, + { + "node": "resources.jobs.job_b.permissions", + "label": "${resources.jobs.job_b.permissions[0].level}" + }, + { + "node": "resources.jobs.job_b.permissions", + "label": "${resources.jobs.job_b.permissions[1].level}" + } + ], + "action": "create", + "new_state": { + "value": { + "object_id": "", + "_": [ + { + "level": "CAN_VIEW", + "group_name": "team-alpha" + }, + { + "level": "CAN_MANAGE", + "group_name": "team-beta" + }, + { + "level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + }, + "vars": { + "object_id": "/jobs/${resources.jobs.job_a.id}" + } + } + }, + "resources.jobs.job_b": { + "action": "create", + "new_state": { + "value": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "job B", + "queue": { + "enabled": true + } + } + } + }, + "resources.jobs.job_b.permissions": { + "depends_on": [ + { + "node": "resources.jobs.job_b", + "label": "${resources.jobs.job_b.id}" + } + ], + "action": "create", + "new_state": { + "value": { + "object_id": "", + "_": [ + { + "level": "CAN_VIEW", + "group_name": "viewers" + }, + { + "level": "CAN_MANAGE", + "group_name": "admins" + }, + { + "level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + }, + "vars": { + "object_id": "/jobs/${resources.jobs.job_b.id}" + } + } + } + } +} diff --git a/acceptance/bundle/resource_deps/permission_ref/out.test.toml b/acceptance/bundle/resource_deps/permission_ref/out.test.toml new file mode 100644 index 0000000000..54146af564 --- /dev/null +++ b/acceptance/bundle/resource_deps/permission_ref/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resource_deps/permission_ref/output.txt b/acceptance/bundle/resource_deps/permission_ref/output.txt new file mode 100644 index 0000000000..cddd86442d --- /dev/null +++ b/acceptance/bundle/resource_deps/permission_ref/output.txt @@ -0,0 +1,34 @@ + +>>> [CLI] bundle plan +Warning: invalid value "${resources.jobs.job_b.permissions[0].level}" for enum field. Valid values are [CAN_MANAGE CAN_MANAGE_RUN CAN_VIEW IS_OWNER] + at resources.jobs.job_a.permissions[0].level + +Warning: invalid value "${resources.jobs.job_b.permissions[1].level}" for enum field. Valid values are [CAN_MANAGE CAN_MANAGE_RUN CAN_VIEW IS_OWNER] + at resources.jobs.job_a.permissions[1].level + +create jobs.job_a +create jobs.job_a.permissions +create jobs.job_b +create jobs.job_b.permissions + +Plan: 4 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle plan -o json +Warning: invalid value "${resources.jobs.job_b.permissions[0].level}" for enum field. Valid values are [CAN_MANAGE CAN_MANAGE_RUN CAN_VIEW IS_OWNER] + at resources.jobs.job_a.permissions[0].level + +Warning: invalid value "${resources.jobs.job_b.permissions[1].level}" for enum field. Valid values are [CAN_MANAGE CAN_MANAGE_RUN CAN_VIEW IS_OWNER] + at resources.jobs.job_a.permissions[1].level + + +>>> [CLI] bundle deploy +Warning: invalid value "${resources.jobs.job_b.permissions[0].level}" for enum field. Valid values are [CAN_MANAGE CAN_MANAGE_RUN CAN_VIEW IS_OWNER] + at resources.jobs.job_a.permissions[0].level + +Warning: invalid value "${resources.jobs.job_b.permissions[1].level}" for enum field. Valid values are [CAN_MANAGE CAN_MANAGE_RUN CAN_VIEW IS_OWNER] + at resources.jobs.job_a.permissions[1].level + +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! diff --git a/acceptance/bundle/resource_deps/permission_ref/script b/acceptance/bundle/resource_deps/permission_ref/script new file mode 100644 index 0000000000..82800b1654 --- /dev/null +++ b/acceptance/bundle/resource_deps/permission_ref/script @@ -0,0 +1,4 @@ + +trace $CLI bundle plan +trace $CLI bundle plan -o json > out.plan_create.$DATABRICKS_BUNDLE_ENGINE.json +trace $CLI bundle deploy diff --git a/acceptance/bundle/resource_deps/permission_ref/test.toml b/acceptance/bundle/resource_deps/permission_ref/test.toml new file mode 100644 index 0000000000..790c13e6dc --- /dev/null +++ b/acceptance/bundle/resource_deps/permission_ref/test.toml @@ -0,0 +1,4 @@ +RecordRequests = false + +[EnvMatrix] +DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/jobs/big_id/out.state.direct.json b/acceptance/bundle/resources/jobs/big_id/out.state.direct.json index 3a904b586a..f8cf0ce5bf 100644 --- a/acceptance/bundle/resources/jobs/big_id/out.state.direct.json +++ b/acceptance/bundle/resources/jobs/big_id/out.state.direct.json @@ -1,5 +1,5 @@ { - "state_version": 1, + "state_version": 2, "cli_version": "[DEV_VERSION]", "lineage": "[UUID]", "serial": 1, diff --git a/acceptance/bundle/resources/jobs/update/out.state.direct.json b/acceptance/bundle/resources/jobs/update/out.state.direct.json index 86f3d399d3..785c0c1338 100644 --- a/acceptance/bundle/resources/jobs/update/out.state.direct.json +++ b/acceptance/bundle/resources/jobs/update/out.state.direct.json @@ -1,5 +1,5 @@ { - "state_version": 1, + "state_version": 2, "cli_version": "[DEV_VERSION]", "lineage": "[UUID]", "serial": 1, diff --git a/acceptance/bundle/resources/model_serving_endpoints/basic/out.first-plan.direct.json b/acceptance/bundle/resources/model_serving_endpoints/basic/out.first-plan.direct.json index cad526a380..970b1fa721 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/basic/out.first-plan.direct.json +++ b/acceptance/bundle/resources/model_serving_endpoints/basic/out.first-plan.direct.json @@ -21,13 +21,13 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "deco-test-user@databricks.com" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/model_serving_endpoints/basic/out.second-plan.direct.json b/acceptance/bundle/resources/model_serving_endpoints/basic/out.second-plan.direct.json index 274a9faceb..9f76bc813c 100644 --- a/acceptance/bundle/resources/model_serving_endpoints/basic/out.second-plan.direct.json +++ b/acceptance/bundle/resources/model_serving_endpoints/basic/out.second-plan.direct.json @@ -59,13 +59,13 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "deco-test-user@databricks.com" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[USERNAME]" } ] @@ -76,13 +76,13 @@ }, "remote_state": { "object_id": "/serving-endpoints/[ENDPOINT_ID_1]", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "deco-test-user@databricks.com" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/apps/current_can_manage/out.plan.direct.json b/acceptance/bundle/resources/permissions/apps/current_can_manage/out.plan.direct.json index f2d859ae88..8401d7bf70 100644 --- a/acceptance/bundle/resources/permissions/apps/current_can_manage/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/apps/current_can_manage/out.plan.direct.json @@ -22,21 +22,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_USE", + "level": "CAN_USE", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[UUID]" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/apps/other_can_manage/out.plan.direct.json b/acceptance/bundle/resources/permissions/apps/other_can_manage/out.plan.direct.json index f2d859ae88..8401d7bf70 100644 --- a/acceptance/bundle/resources/permissions/apps/other_can_manage/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/apps/other_can_manage/out.plan.direct.json @@ -22,21 +22,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_USE", + "level": "CAN_USE", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[UUID]" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/clusters/current_can_manage/out.plan.direct.json b/acceptance/bundle/resources/permissions/clusters/current_can_manage/out.plan.direct.json index efce8b5ca7..0317c5209f 100644 --- a/acceptance/bundle/resources/permissions/clusters/current_can_manage/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/clusters/current_can_manage/out.plan.direct.json @@ -25,21 +25,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_ATTACH_TO", + "level": "CAN_ATTACH_TO", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[UUID]" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "[USERNAME]" } ] 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 2ff580dfe7..1b2826b1c4 100644 --- a/acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/dashboards/create/out.plan.direct.json @@ -26,13 +26,13 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_READ", + "level": "CAN_READ", "user_name": "deco-test-user@databricks.com" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/database_instances/current_can_manage/out.plan.direct.json b/acceptance/bundle/resources/permissions/database_instances/current_can_manage/out.plan.direct.json index 8785b03dcd..f7326be47b 100644 --- a/acceptance/bundle/resources/permissions/database_instances/current_can_manage/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/database_instances/current_can_manage/out.plan.direct.json @@ -21,21 +21,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_USE", + "level": "CAN_USE", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[UUID]" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/experiments/current_can_manage/out.plan.direct.json b/acceptance/bundle/resources/permissions/experiments/current_can_manage/out.plan.direct.json index f3ccbc42d7..c7a47db2a9 100644 --- a/acceptance/bundle/resources/permissions/experiments/current_can_manage/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/experiments/current_can_manage/out.plan.direct.json @@ -21,21 +21,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_READ", + "level": "CAN_READ", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[UUID]" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/added_remotely/out.plan.direct.json b/acceptance/bundle/resources/permissions/jobs/added_remotely/out.plan.direct.json index c5e456b5c0..5edb4c3166 100644 --- a/acceptance/bundle/resources/permissions/jobs/added_remotely/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/added_remotely/out.plan.direct.json @@ -82,13 +82,13 @@ "new_state": { "value": { "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] @@ -96,27 +96,27 @@ }, "remote_state": { "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "admin-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "admin-team" } ] }, "changes": { - "permissions[group_name='admin-team']": { + "[group_name='admin-team']": { "action": "update", "remote": { - "group_name": "admin-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "admin-team" } } } diff --git a/acceptance/bundle/resources/permissions/jobs/current_can_manage/out.plan.direct.json b/acceptance/bundle/resources/permissions/jobs/current_can_manage/out.plan.direct.json index ae0559c042..d2f96bf86e 100644 --- a/acceptance/bundle/resources/permissions/jobs/current_can_manage/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/current_can_manage/out.plan.direct.json @@ -40,21 +40,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[UUID]" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/current_can_manage_run/out.plan.direct.txt b/acceptance/bundle/resources/permissions/jobs/current_can_manage_run/out.plan.direct.txt index 776d729dee..70f14610fe 100644 --- a/acceptance/bundle/resources/permissions/jobs/current_can_manage_run/out.plan.direct.txt +++ b/acceptance/bundle/resources/permissions/jobs/current_can_manage_run/out.plan.direct.txt @@ -54,9 +54,9 @@ "action": "skip", "remote_state": { "object_id": "/jobs/[NUMID]", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "service_principal_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/current_is_owner/out.plan.direct.json b/acceptance/bundle/resources/permissions/jobs/current_is_owner/out.plan.direct.json index 31d4e8cdf0..c2cc506e9d 100644 --- a/acceptance/bundle/resources/permissions/jobs/current_is_owner/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/current_is_owner/out.plan.direct.json @@ -40,9 +40,9 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/delete_one/cloud/out.plan_create.direct.json b/acceptance/bundle/resources/permissions/jobs/delete_one/cloud/out.plan_create.direct.json index 828f41c032..3ccf8091cc 100644 --- a/acceptance/bundle/resources/permissions/jobs/delete_one/cloud/out.plan_create.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/delete_one/cloud/out.plan_create.direct.json @@ -40,21 +40,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_MANAGE_RUN", + "level": "CAN_MANAGE_RUN", "user_name": "test-dabs-1@databricks.com" }, { - "group_name": "test-dabs-group-1", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "test-dabs-group-1" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "test-dabs-2@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "service_principal_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/delete_one/local/out.plan_create.direct.json b/acceptance/bundle/resources/permissions/jobs/delete_one/local/out.plan_create.direct.json index 828f41c032..3ccf8091cc 100644 --- a/acceptance/bundle/resources/permissions/jobs/delete_one/local/out.plan_create.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/delete_one/local/out.plan_create.direct.json @@ -40,21 +40,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_MANAGE_RUN", + "level": "CAN_MANAGE_RUN", "user_name": "test-dabs-1@databricks.com" }, { - "group_name": "test-dabs-group-1", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "test-dabs-group-1" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "test-dabs-2@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "service_principal_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/delete_one/local/out.plan_update.direct.json b/acceptance/bundle/resources/permissions/jobs/delete_one/local/out.plan_update.direct.json index 1914747039..ce9eb048b2 100644 --- a/acceptance/bundle/resources/permissions/jobs/delete_one/local/out.plan_update.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/delete_one/local/out.plan_update.direct.json @@ -82,17 +82,17 @@ "new_state": { "value": { "object_id": "/jobs/[NUMID]", - "permissions": [ + "_": [ { - "group_name": "test-dabs-group-1", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "test-dabs-group-1" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "test-dabs-2@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "service_principal_name": "[USERNAME]" } ] @@ -100,34 +100,34 @@ }, "remote_state": { "object_id": "/jobs/[NUMID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_MANAGE_RUN", + "level": "CAN_MANAGE_RUN", "user_name": "test-dabs-1@databricks.com" }, { - "group_name": "test-dabs-group-1", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "test-dabs-group-1" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "test-dabs-2@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "service_principal_name": "[USERNAME]" } ] }, "changes": { - "permissions[user_name='test-dabs-1@databricks.com']": { + "[user_name='test-dabs-1@databricks.com']": { "action": "update", "old": { - "permission_level": "CAN_MANAGE_RUN", + "level": "CAN_MANAGE_RUN", "user_name": "test-dabs-1@databricks.com" }, "remote": { - "permission_level": "CAN_MANAGE_RUN", + "level": "CAN_MANAGE_RUN", "user_name": "test-dabs-1@databricks.com" } } diff --git a/acceptance/bundle/resources/permissions/jobs/deleted_remotely/out.plan_create.direct.json b/acceptance/bundle/resources/permissions/jobs/deleted_remotely/out.plan_create.direct.json index 815336b75a..74bdb1b9e4 100644 --- a/acceptance/bundle/resources/permissions/jobs/deleted_remotely/out.plan_create.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/deleted_remotely/out.plan_create.direct.json @@ -40,17 +40,17 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/deleted_remotely/out.plan_restore.direct.json b/acceptance/bundle/resources/permissions/jobs/deleted_remotely/out.plan_restore.direct.json index 6779ef9018..2b5b960ca7 100644 --- a/acceptance/bundle/resources/permissions/jobs/deleted_remotely/out.plan_restore.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/deleted_remotely/out.plan_restore.direct.json @@ -82,17 +82,17 @@ "new_state": { "value": { "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] @@ -100,33 +100,33 @@ }, "remote_state": { "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] }, "changes": { - "permissions[group_name='data-team']": { + "[group_name='data-team']": { "action": "update", "old": { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, "new": { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" } }, - "permissions[user_name='viewer@example.com']": { + "[user_name='viewer@example.com']": { "action": "update", "old": { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, "new": { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" } } diff --git a/acceptance/bundle/resources/permissions/jobs/other_can_manage/out.plan.direct.json b/acceptance/bundle/resources/permissions/jobs/other_can_manage/out.plan.direct.json index ae0559c042..d2f96bf86e 100644 --- a/acceptance/bundle/resources/permissions/jobs/other_can_manage/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/other_can_manage/out.plan.direct.json @@ -40,21 +40,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[UUID]" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/other_is_owner/out.plan.direct.json b/acceptance/bundle/resources/permissions/jobs/other_is_owner/out.plan.direct.json index 9a034ea3bb..bc37fbd425 100644 --- a/acceptance/bundle/resources/permissions/jobs/other_is_owner/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/other_is_owner/out.plan.direct.json @@ -40,13 +40,13 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "other_user@databricks.com" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/update/out.plan_create.direct.json b/acceptance/bundle/resources/permissions/jobs/update/out.plan_create.direct.json index df85b07b94..439463d439 100644 --- a/acceptance/bundle/resources/permissions/jobs/update/out.plan_create.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/update/out.plan_create.direct.json @@ -40,17 +40,17 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/update/out.plan_delete_all.direct.json b/acceptance/bundle/resources/permissions/jobs/update/out.plan_delete_all.direct.json index 06e3eed8c7..5543181159 100644 --- a/acceptance/bundle/resources/permissions/jobs/update/out.plan_delete_all.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/update/out.plan_delete_all.direct.json @@ -81,13 +81,13 @@ "action": "delete", "remote_state": { "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "viewer@example.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/update/out.plan_delete_one.direct.json b/acceptance/bundle/resources/permissions/jobs/update/out.plan_delete_one.direct.json index c15dd9a21c..a9c54fb77b 100644 --- a/acceptance/bundle/resources/permissions/jobs/update/out.plan_delete_one.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/update/out.plan_delete_one.direct.json @@ -82,13 +82,13 @@ "new_state": { "value": { "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "viewer@example.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] @@ -96,31 +96,31 @@ }, "remote_state": { "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] }, "changes": { - "permissions[group_name='data-team']": { + "[group_name='data-team']": { "action": "update", "old": { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, "remote": { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" } } } diff --git a/acceptance/bundle/resources/permissions/jobs/update/out.plan_post_create.direct.json b/acceptance/bundle/resources/permissions/jobs/update/out.plan_post_create.direct.json index 44682af8c0..7459bb83b3 100644 --- a/acceptance/bundle/resources/permissions/jobs/update/out.plan_post_create.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/update/out.plan_post_create.direct.json @@ -81,17 +81,17 @@ "action": "skip", "remote_state": { "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/update/out.plan_restore.direct.json b/acceptance/bundle/resources/permissions/jobs/update/out.plan_restore.direct.json index b46b11176f..25e5371736 100644 --- a/acceptance/bundle/resources/permissions/jobs/update/out.plan_restore.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/update/out.plan_restore.direct.json @@ -82,17 +82,17 @@ "new_state": { "value": { "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/update/out.plan_set_empty.direct.json b/acceptance/bundle/resources/permissions/jobs/update/out.plan_set_empty.direct.json index 0d192dd875..b05402caae 100644 --- a/acceptance/bundle/resources/permissions/jobs/update/out.plan_set_empty.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/update/out.plan_set_empty.direct.json @@ -81,17 +81,17 @@ "action": "skip", "remote_state": { "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/jobs/update/out.plan_update.direct.json b/acceptance/bundle/resources/permissions/jobs/update/out.plan_update.direct.json index 20cbccb035..d16a504956 100644 --- a/acceptance/bundle/resources/permissions/jobs/update/out.plan_update.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/update/out.plan_update.direct.json @@ -82,17 +82,17 @@ "new_state": { "value": { "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] @@ -100,23 +100,23 @@ }, "remote_state": { "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] }, "changes": { - "permissions[user_name='viewer@example.com'].permission_level": { + "[user_name='viewer@example.com'].level": { "action": "update", "old": "CAN_VIEW", "new": "CAN_MANAGE", diff --git a/acceptance/bundle/resources/permissions/jobs/viewers/out.plan.direct.json b/acceptance/bundle/resources/permissions/jobs/viewers/out.plan.direct.json index 13e39b3187..13f9e13cce 100644 --- a/acceptance/bundle/resources/permissions/jobs/viewers/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/viewers/out.plan.direct.json @@ -40,21 +40,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_VIEW" + "level": "CAN_VIEW", + "group_name": "data-team" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "service_principal_name": "[UUID]" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/models/current_can_manage/out.plan.direct.json b/acceptance/bundle/resources/permissions/models/current_can_manage/out.plan.direct.json index 7708fd3a46..5cccc073f1 100644 --- a/acceptance/bundle/resources/permissions/models/current_can_manage/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/models/current_can_manage/out.plan.direct.json @@ -21,21 +21,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_READ", + "level": "CAN_READ", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[UUID]" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/pipelines/current_can_manage/out.plan.direct.json b/acceptance/bundle/resources/permissions/pipelines/current_can_manage/out.plan.direct.json index 8c83462969..d9861e0827 100644 --- a/acceptance/bundle/resources/permissions/pipelines/current_can_manage/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/pipelines/current_can_manage/out.plan.direct.json @@ -27,21 +27,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[UUID]" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/pipelines/current_is_owner/out.plan.direct.json b/acceptance/bundle/resources/permissions/pipelines/current_is_owner/out.plan.direct.json index 71565abd4d..aac995c1b3 100644 --- a/acceptance/bundle/resources/permissions/pipelines/current_is_owner/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/pipelines/current_is_owner/out.plan.direct.json @@ -27,9 +27,9 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/pipelines/other_can_manage/out.plan.direct.json b/acceptance/bundle/resources/permissions/pipelines/other_can_manage/out.plan.direct.json index 8c83462969..d9861e0827 100644 --- a/acceptance/bundle/resources/permissions/pipelines/other_can_manage/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/pipelines/other_can_manage/out.plan.direct.json @@ -27,21 +27,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[UUID]" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/pipelines/other_is_owner/out.plan.direct.json b/acceptance/bundle/resources/permissions/pipelines/other_is_owner/out.plan.direct.json index 7dda6a281c..9e6ce1796d 100644 --- a/acceptance/bundle/resources/permissions/pipelines/other_is_owner/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/pipelines/other_is_owner/out.plan.direct.json @@ -27,13 +27,13 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "other_user@databricks.com" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/pipelines/update/out.plan_create.direct.json b/acceptance/bundle/resources/permissions/pipelines/update/out.plan_create.direct.json index 4fd114508f..2db5e7fc9f 100644 --- a/acceptance/bundle/resources/permissions/pipelines/update/out.plan_create.direct.json +++ b/acceptance/bundle/resources/permissions/pipelines/update/out.plan_create.direct.json @@ -27,17 +27,17 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/pipelines/update/out.plan_delete_all.direct.json b/acceptance/bundle/resources/permissions/pipelines/update/out.plan_delete_all.direct.json index af3718c59f..d2d5bfc474 100644 --- a/acceptance/bundle/resources/permissions/pipelines/update/out.plan_delete_all.direct.json +++ b/acceptance/bundle/resources/permissions/pipelines/update/out.plan_delete_all.direct.json @@ -40,13 +40,13 @@ "action": "delete", "remote_state": { "object_id": "/pipelines/[FOO_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "viewer@example.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/pipelines/update/out.plan_delete_one.direct.json b/acceptance/bundle/resources/permissions/pipelines/update/out.plan_delete_one.direct.json index f11c8a77fc..c848971afc 100644 --- a/acceptance/bundle/resources/permissions/pipelines/update/out.plan_delete_one.direct.json +++ b/acceptance/bundle/resources/permissions/pipelines/update/out.plan_delete_one.direct.json @@ -41,13 +41,13 @@ "new_state": { "value": { "object_id": "/pipelines/[FOO_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "viewer@example.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] @@ -55,31 +55,31 @@ }, "remote_state": { "object_id": "/pipelines/[FOO_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] }, "changes": { - "permissions[group_name='data-team']": { + "[group_name='data-team']": { "action": "update", "old": { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, "remote": { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" } } } diff --git a/acceptance/bundle/resources/permissions/pipelines/update/out.plan_restore.direct.json b/acceptance/bundle/resources/permissions/pipelines/update/out.plan_restore.direct.json index fad11f3014..46fc67eaa6 100644 --- a/acceptance/bundle/resources/permissions/pipelines/update/out.plan_restore.direct.json +++ b/acceptance/bundle/resources/permissions/pipelines/update/out.plan_restore.direct.json @@ -41,17 +41,17 @@ "new_state": { "value": { "object_id": "/pipelines/[FOO_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/pipelines/update/out.plan_update.direct.json b/acceptance/bundle/resources/permissions/pipelines/update/out.plan_update.direct.json index 6fd7896b3d..2a0f70269a 100644 --- a/acceptance/bundle/resources/permissions/pipelines/update/out.plan_update.direct.json +++ b/acceptance/bundle/resources/permissions/pipelines/update/out.plan_update.direct.json @@ -41,17 +41,17 @@ "new_state": { "value": { "object_id": "/pipelines/[FOO_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] @@ -59,23 +59,23 @@ }, "remote_state": { "object_id": "/pipelines/[FOO_ID]", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] }, "changes": { - "permissions[user_name='viewer@example.com'].permission_level": { + "[user_name='viewer@example.com'].level": { "action": "update", "old": "CAN_VIEW", "new": "CAN_MANAGE", diff --git a/acceptance/bundle/resources/permissions/postgres_projects/current_can_manage/out.plan.direct.json b/acceptance/bundle/resources/permissions/postgres_projects/current_can_manage/out.plan.direct.json index 5dc10317fe..be9d192a15 100644 --- a/acceptance/bundle/resources/permissions/postgres_projects/current_can_manage/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/postgres_projects/current_can_manage/out.plan.direct.json @@ -22,21 +22,21 @@ "new_state": { "value": { "object_id": "/database-projects/test-project", - "permissions": [ + "_": [ { - "permission_level": "CAN_USE", + "level": "CAN_USE", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[UUID]" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/sql_warehouses/current_can_manage/out.plan.direct.json b/acceptance/bundle/resources/permissions/sql_warehouses/current_can_manage/out.plan.direct.json index f97adc2fce..6c0a2c5c16 100644 --- a/acceptance/bundle/resources/permissions/sql_warehouses/current_can_manage/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/sql_warehouses/current_can_manage/out.plan.direct.json @@ -26,21 +26,21 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "viewer@example.com" }, { - "group_name": "data-team", - "permission_level": "CAN_MANAGE" + "level": "CAN_MANAGE", + "group_name": "data-team" }, { - "permission_level": "CAN_MANAGE", + "level": "CAN_MANAGE", "service_principal_name": "[UUID]" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/resources/permissions/target_permissions/out.plan.direct.json b/acceptance/bundle/resources/permissions/target_permissions/out.plan.direct.json index e8bd55c924..ade7d8f788 100644 --- a/acceptance/bundle/resources/permissions/target_permissions/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/target_permissions/out.plan.direct.json @@ -40,9 +40,9 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_service_principal_name.direct.json b/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_service_principal_name.direct.json index c94f48afcb..15d5cbb041 100644 --- a/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_service_principal_name.direct.json +++ b/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_service_principal_name.direct.json @@ -37,17 +37,17 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "service_principal_name": "my_service_principal" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "different@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_service_principal_name_different.direct.json b/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_service_principal_name_different.direct.json index 52a7979a13..43f9e69161 100644 --- a/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_service_principal_name_different.direct.json +++ b/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_service_principal_name_different.direct.json @@ -37,17 +37,17 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "service_principal_name": "my_service_principal" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "different@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_user_name.direct.json b/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_user_name.direct.json index 36c220a456..1ff7fd1b93 100644 --- a/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_user_name.direct.json +++ b/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_user_name.direct.json @@ -37,17 +37,17 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "service_principal_name": "my_service_principal" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "different@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_user_name_different.direct.json b/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_user_name_different.direct.json index 01ac33df56..b2e1769470 100644 --- a/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_user_name_different.direct.json +++ b/acceptance/bundle/run_as/pipelines/regular_user/out.plan_t_user_name_different.direct.json @@ -37,17 +37,17 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "service_principal_name": "my_service_principal" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "different@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_service_principal_name.direct.json b/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_service_principal_name.direct.json index 9adb498e84..2e12d667f2 100644 --- a/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_service_principal_name.direct.json +++ b/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_service_principal_name.direct.json @@ -37,17 +37,17 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "service_principal_name": "my_service_principal" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "different@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "service_principal_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_service_principal_name_different.direct.json b/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_service_principal_name_different.direct.json index e77a317889..4fcbcecfd5 100644 --- a/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_service_principal_name_different.direct.json +++ b/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_service_principal_name_different.direct.json @@ -37,17 +37,17 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "service_principal_name": "my_service_principal" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "different@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "service_principal_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_user_name.direct.json b/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_user_name.direct.json index 670df6b321..3c9d7d31d9 100644 --- a/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_user_name.direct.json +++ b/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_user_name.direct.json @@ -37,17 +37,17 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "service_principal_name": "my_service_principal" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "different@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "service_principal_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_user_name_different.direct.json b/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_user_name_different.direct.json index 001b6a03d7..86df3f30a5 100644 --- a/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_user_name_different.direct.json +++ b/acceptance/bundle/run_as/pipelines/service_principal/out.plan_t_user_name_different.direct.json @@ -37,17 +37,17 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "service_principal_name": "my_service_principal" }, { - "permission_level": "CAN_VIEW", + "level": "CAN_VIEW", "user_name": "different@databricks.com" }, { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "service_principal_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/state/permission_level_migration/databricks.yml b/acceptance/bundle/state/permission_level_migration/databricks.yml new file mode 100644 index 0000000000..a9ed3a31a8 --- /dev/null +++ b/acceptance/bundle/state/permission_level_migration/databricks.yml @@ -0,0 +1,10 @@ +bundle: + name: test-bundle + +resources: + jobs: + my_job: + name: "my job" + permissions: + - level: CAN_VIEW + group_name: viewers diff --git a/acceptance/bundle/state/permission_level_migration/out.plan.direct.json b/acceptance/bundle/state/permission_level_migration/out.plan.direct.json new file mode 100644 index 0000000000..e616aed390 --- /dev/null +++ b/acceptance/bundle/state/permission_level_migration/out.plan.direct.json @@ -0,0 +1,90 @@ +{ + "plan_version": 2, + "cli_version": "[DEV_VERSION]", + "lineage": "test-lineage", + "serial": 1, + "plan": { + "resources.jobs.my_job": { + "action": "create", + "new_state": { + "value": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "my job", + "queue": { + "enabled": true + } + } + } + }, + "resources.jobs.my_job.permissions": { + "depends_on": [ + { + "node": "resources.jobs.my_job", + "label": "${resources.jobs.my_job.id}" + } + ], + "action": "update", + "new_state": { + "value": { + "object_id": "", + "_": [ + { + "level": "CAN_VIEW", + "group_name": "viewers" + }, + { + "level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + }, + "vars": { + "object_id": "/jobs/${resources.jobs.my_job.id}" + } + }, + "remote_state": { + "object_id": "/jobs/123" + }, + "changes": { + "": { + "action": "update", + "old": { + "object_id": "/jobs/123", + "_": [ + { + "level": "CAN_VIEW", + "group_name": "viewers" + }, + { + "level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + }, + "new": [ + { + "level": "CAN_VIEW", + "group_name": "viewers" + }, + { + "level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + }, + "object_id": { + "action": "update", + "old": "/jobs/123", + "new": "", + "remote": "/jobs/123" + } + } + } + } +} diff --git a/acceptance/bundle/state/permission_level_migration/out.test.toml b/acceptance/bundle/state/permission_level_migration/out.test.toml new file mode 100644 index 0000000000..54146af564 --- /dev/null +++ b/acceptance/bundle/state/permission_level_migration/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/state/permission_level_migration/output.txt b/acceptance/bundle/state/permission_level_migration/output.txt new file mode 100644 index 0000000000..f3d0b097de --- /dev/null +++ b/acceptance/bundle/state/permission_level_migration/output.txt @@ -0,0 +1,59 @@ + +=== Plan with old v1 state +>>> [CLI] bundle plan -o json + +=== Deploy (migrates state) +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +=== Print state after deploy +>>> print_state.py +{ + "state_version": 2, + "cli_version": "[DEV_VERSION]", + "lineage": "test-lineage", + "serial": 2, + "state": { + "resources.jobs.my_job": { + "__id__": "[NUMID]", + "state": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "my job", + "queue": { + "enabled": true + } + } + }, + "resources.jobs.my_job.permissions": { + "__id__": "/jobs/123", + "state": { + "object_id": "/jobs/[NUMID]", + "_": [ + { + "level": "CAN_VIEW", + "group_name": "viewers" + }, + { + "level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + }, + "depends_on": [ + { + "node": "resources.jobs.my_job", + "label": "${resources.jobs.my_job.id}" + } + ] + } + } +} diff --git a/acceptance/bundle/state/permission_level_migration/resources.v1.json b/acceptance/bundle/state/permission_level_migration/resources.v1.json new file mode 100644 index 0000000000..78766b3be9 --- /dev/null +++ b/acceptance/bundle/state/permission_level_migration/resources.v1.json @@ -0,0 +1,46 @@ +{ + "state_version": 1, + "cli_version": "0.0.0-dev", + "lineage": "test-lineage", + "serial": 1, + "state": { + "resources.jobs.my_job": { + "__id__": "123", + "state": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/tester@databricks.com/.bundle/test-bundle/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "my job", + "queue": { + "enabled": true + } + } + }, + "resources.jobs.my_job.permissions": { + "__id__": "/jobs/123", + "state": { + "object_id": "/jobs/123", + "permissions": [ + { + "permission_level": "CAN_VIEW", + "group_name": "viewers" + }, + { + "permission_level": "IS_OWNER", + "user_name": "tester@databricks.com" + } + ] + }, + "depends_on": [ + { + "node": "resources.jobs.my_job", + "label": "${resources.jobs.my_job.id}" + } + ] + } + } +} diff --git a/acceptance/bundle/state/permission_level_migration/script b/acceptance/bundle/state/permission_level_migration/script new file mode 100644 index 0000000000..c317985fc8 --- /dev/null +++ b/acceptance/bundle/state/permission_level_migration/script @@ -0,0 +1,18 @@ + +# Test that old v1 state files (from main) are migrated to v2. +# Old format used "permissions" key with "permission_level" field. +# New format uses "_" key with "level" field. + +mkdir -p .databricks/bundle/default + +# Copy state file in old v1 format (as produced by main branch) +cp resources.v1.json .databricks/bundle/default/resources.json + +title "Plan with old v1 state" +trace $CLI bundle plan -o json > out.plan.direct.json + +title "Deploy (migrates state)" +trace $CLI bundle deploy + +title "Print state after deploy" +trace print_state.py diff --git a/acceptance/bundle/state/permission_level_migration/test.toml b/acceptance/bundle/state/permission_level_migration/test.toml new file mode 100644 index 0000000000..7fc493d51a --- /dev/null +++ b/acceptance/bundle/state/permission_level_migration/test.toml @@ -0,0 +1,4 @@ +Ignore = [".databricks"] + +[EnvMatrix] +DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/templates/default-python/classic/out.plan_after_deploy_prod.direct.json b/acceptance/bundle/templates/default-python/classic/out.plan_after_deploy_prod.direct.json index d31f339a20..755f863134 100644 --- a/acceptance/bundle/templates/default-python/classic/out.plan_after_deploy_prod.direct.json +++ b/acceptance/bundle/templates/default-python/classic/out.plan_after_deploy_prod.direct.json @@ -200,9 +200,9 @@ "action": "skip", "remote_state": { "object_id": "/jobs/[NUMID]", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] @@ -258,9 +258,9 @@ "action": "skip", "remote_state": { "object_id": "/pipelines/[UUID]", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/templates/default-python/classic/out.plan_prod.direct.json b/acceptance/bundle/templates/default-python/classic/out.plan_prod.direct.json index aecdcd217c..08b367d6f5 100644 --- a/acceptance/bundle/templates/default-python/classic/out.plan_prod.direct.json +++ b/acceptance/bundle/templates/default-python/classic/out.plan_prod.direct.json @@ -120,9 +120,9 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] @@ -171,9 +171,9 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/templates/default-python/integration_classic/out.plan_prod.direct.json b/acceptance/bundle/templates/default-python/integration_classic/out.plan_prod.direct.json index 05c2ac4790..1b3d75d9c1 100644 --- a/acceptance/bundle/templates/default-python/integration_classic/out.plan_prod.direct.json +++ b/acceptance/bundle/templates/default-python/integration_classic/out.plan_prod.direct.json @@ -120,9 +120,9 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "service_principal_name": "[USERNAME]" } ] @@ -171,9 +171,9 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "service_principal_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/templates/default-python/serverless/out.plan_after_deploy_prod.direct.json b/acceptance/bundle/templates/default-python/serverless/out.plan_after_deploy_prod.direct.json index 812094baeb..138b2c4a6f 100644 --- a/acceptance/bundle/templates/default-python/serverless/out.plan_after_deploy_prod.direct.json +++ b/acceptance/bundle/templates/default-python/serverless/out.plan_after_deploy_prod.direct.json @@ -186,9 +186,9 @@ "action": "skip", "remote_state": { "object_id": "/jobs/[NUMID]", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] @@ -238,9 +238,9 @@ "action": "skip", "remote_state": { "object_id": "/pipelines/[UUID]", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/templates/default-python/serverless/out.plan_prod.direct.json b/acceptance/bundle/templates/default-python/serverless/out.plan_prod.direct.json index 9741ec53f6..f57a1e3e09 100644 --- a/acceptance/bundle/templates/default-python/serverless/out.plan_prod.direct.json +++ b/acceptance/bundle/templates/default-python/serverless/out.plan_prod.direct.json @@ -106,9 +106,9 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] @@ -159,9 +159,9 @@ "new_state": { "value": { "object_id": "", - "permissions": [ + "_": [ { - "permission_level": "IS_OWNER", + "level": "IS_OWNER", "user_name": "[USERNAME]" } ] diff --git a/acceptance/bundle/user_agent/simple/out.requests.deploy.direct.json b/acceptance/bundle/user_agent/simple/out.requests.deploy.direct.json index 2f7aa9843c..d4543d353a 100644 --- a/acceptance/bundle/user_agent/simple/out.requests.deploy.direct.json +++ b/acceptance/bundle/user_agent/simple/out.requests.deploy.direct.json @@ -222,7 +222,7 @@ "overwrite": "true" }, "body": { - "state_version": 1, + "state_version": 2, "cli_version": "[DEV_VERSION]", "lineage": "[UUID]", "serial": 1, diff --git a/bundle/direct/bundle_apply.go b/bundle/direct/bundle_apply.go index 18c415504d..77d98a8a1f 100644 --- a/bundle/direct/bundle_apply.go +++ b/bundle/direct/bundle_apply.go @@ -160,9 +160,7 @@ func (b *DeploymentBundle) Apply(ctx context.Context, client *databricks.Workspa } func (b *DeploymentBundle) LookupReferencePostDeploy(ctx context.Context, path *structpath.PathNode) (any, error) { - // TODO: Prefix(3) assumes resources.jobs.foo but not resources.jobs.foo.permissions - targetResourceKey := path.Prefix(3).String() - fieldPath := path.SkipPrefix(3) + targetResourceKey, fieldPath := splitResourcePath(path) fieldPathS := fieldPath.String() targetEntry, err := b.Plan.ReadLockEntry(targetResourceKey) diff --git a/bundle/direct/bundle_plan.go b/bundle/direct/bundle_plan.go index 5c518adcc9..9ee4347450 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -569,11 +569,22 @@ func isEmptyStruct(rv reflect.Value) bool { return true } -func (b *DeploymentBundle) LookupReferencePreDeploy(ctx context.Context, path *structpath.PathNode) (any, error) { - // TODO: Prefix(3) assumes resources.jobs.foo but not resources.jobs.foo.permissions - targetResourceKey := path.Prefix(3).String() +// splitResourcePath splits a reference path into resource key and field path. +// For regular resources like "resources.jobs.foo.name", returns ("resources.jobs.foo", "name"). +// For sub-resources like "resources.jobs.foo.permissions[0].level", returns ("resources.jobs.foo.permissions", "[0].level"). +func splitResourcePath(path *structpath.PathNode) (string, *structpath.PathNode) { + // Check if the 4th component is "permissions" or "grants" (sub-resource) + if path.Len() > 4 { + first := path.SkipPrefix(3).Prefix(1) + if key, ok := first.StringKey(); ok && (key == "permissions" || key == "grants") { + return path.Prefix(4).String(), path.SkipPrefix(4) + } + } + return path.Prefix(3).String(), path.SkipPrefix(3) +} - fieldPath := path.SkipPrefix(3) +func (b *DeploymentBundle) LookupReferencePreDeploy(ctx context.Context, path *structpath.PathNode) (any, error) { + targetResourceKey, fieldPath := splitResourcePath(path) fieldPathS := fieldPath.String() targetEntry, err := b.Plan.ReadLockEntry(targetResourceKey) diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index 0c3d95da8a..538bc05d47 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -21,7 +21,6 @@ import ( "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" "github.com/databricks/databricks-sdk-go/service/ml" "github.com/databricks/databricks-sdk-go/service/pipelines" @@ -279,9 +278,9 @@ var testDeps = map[string]prepareWorkspace{ return &PermissionsState{ ObjectID: "/jobs/" + strconv.FormatInt(resp.JobId, 10), - Permissions: []iam.AccessControlRequest{{ - PermissionLevel: "IS_OWNER", - UserName: "user@example.com", + EmbeddedSlice: []StatePermission{{ + Level: "IS_OWNER", + UserName: "user@example.com", }}, }, nil }, @@ -296,9 +295,9 @@ var testDeps = map[string]prepareWorkspace{ return &PermissionsState{ ObjectID: "/pipelines/" + resp.PipelineId, - Permissions: []iam.AccessControlRequest{{ - PermissionLevel: "CAN_MANAGE", - UserName: "user@example.com", + EmbeddedSlice: []StatePermission{{ + Level: "CAN_MANAGE", + UserName: "user@example.com", }}, }, nil }, @@ -314,9 +313,9 @@ var testDeps = map[string]prepareWorkspace{ return &PermissionsState{ ObjectID: "/registered-models/" + resp.RegisteredModel.Name, - Permissions: []iam.AccessControlRequest{{ - PermissionLevel: "CAN_MANAGE", - UserName: "user@example.com", + EmbeddedSlice: []StatePermission{{ + Level: "CAN_MANAGE", + UserName: "user@example.com", }}, }, nil }, @@ -331,9 +330,9 @@ var testDeps = map[string]prepareWorkspace{ return &PermissionsState{ ObjectID: "/experiments/" + resp.ExperimentId, - Permissions: []iam.AccessControlRequest{{ - PermissionLevel: "CAN_MANAGE", - UserName: "user@example.com", + EmbeddedSlice: []StatePermission{{ + Level: "CAN_MANAGE", + UserName: "user@example.com", }}, }, nil }, @@ -341,9 +340,9 @@ var testDeps = map[string]prepareWorkspace{ "clusters.permissions": func(ctx context.Context, client *databricks.WorkspaceClient) (any, error) { return &PermissionsState{ ObjectID: "/clusters/cluster-permissions", - Permissions: []iam.AccessControlRequest{{ - PermissionLevel: "CAN_MANAGE", - UserName: "user@example.com", + EmbeddedSlice: []StatePermission{{ + Level: "CAN_MANAGE", + UserName: "user@example.com", }}, }, nil }, @@ -360,9 +359,9 @@ var testDeps = map[string]prepareWorkspace{ return &PermissionsState{ ObjectID: "/apps/" + waiter.Response.Name, - Permissions: []iam.AccessControlRequest{{ - PermissionLevel: "CAN_MANAGE", - UserName: "user@example.com", + EmbeddedSlice: []StatePermission{{ + Level: "CAN_MANAGE", + UserName: "user@example.com", }}, }, nil }, @@ -370,9 +369,9 @@ var testDeps = map[string]prepareWorkspace{ "sql_warehouses.permissions": func(ctx context.Context, client *databricks.WorkspaceClient) (any, error) { return &PermissionsState{ ObjectID: "/sql/warehouses/warehouse-permissions", - Permissions: []iam.AccessControlRequest{{ - PermissionLevel: "CAN_MANAGE", - UserName: "user@example.com", + EmbeddedSlice: []StatePermission{{ + Level: "CAN_MANAGE", + UserName: "user@example.com", }}, }, nil }, @@ -389,9 +388,9 @@ var testDeps = map[string]prepareWorkspace{ return &PermissionsState{ ObjectID: "/database-instances/" + waiter.Response.Name, - Permissions: []iam.AccessControlRequest{{ - PermissionLevel: "CAN_MANAGE", - UserName: "user@example.com", + EmbeddedSlice: []StatePermission{{ + Level: "CAN_MANAGE", + UserName: "user@example.com", }}, }, nil }, @@ -411,9 +410,9 @@ var testDeps = map[string]prepareWorkspace{ components, _ := ParsePostgresName(result.Name) return &PermissionsState{ ObjectID: "/database-projects/" + components.ProjectID, - Permissions: []iam.AccessControlRequest{{ - PermissionLevel: "CAN_MANAGE", - UserName: "user@example.com", + EmbeddedSlice: []StatePermission{{ + Level: "CAN_MANAGE", + UserName: "user@example.com", }}, }, nil }, @@ -440,9 +439,9 @@ var testDeps = map[string]prepareWorkspace{ return &PermissionsState{ ObjectID: "/dashboards/" + resp.DashboardId, - Permissions: []iam.AccessControlRequest{{ - PermissionLevel: "CAN_MANAGE", - UserName: "user@example.com", + EmbeddedSlice: []StatePermission{{ + Level: "CAN_MANAGE", + UserName: "user@example.com", }}, }, nil }, @@ -467,9 +466,9 @@ var testDeps = map[string]prepareWorkspace{ return &PermissionsState{ ObjectID: "/serving-endpoints/" + waiter.Response.Name, - Permissions: []iam.AccessControlRequest{{ - PermissionLevel: "CAN_MANAGE", - UserName: "user@example.com", + EmbeddedSlice: []StatePermission{{ + Level: "CAN_MANAGE", + UserName: "user@example.com", }}, }, nil }, @@ -498,9 +497,9 @@ var testDeps = map[string]prepareWorkspace{ return &PermissionsState{ ObjectID: "/alertsv2/" + resp.Id, - Permissions: []iam.AccessControlRequest{{ - PermissionLevel: "CAN_MANAGE", - UserName: "user@example.com", + EmbeddedSlice: []StatePermission{{ + Level: "CAN_MANAGE", + UserName: "user@example.com", }}, }, nil }, diff --git a/bundle/direct/dresources/permissions.go b/bundle/direct/dresources/permissions.go index 724c38eaef..23f91a7af9 100644 --- a/bundle/direct/dresources/permissions.go +++ b/bundle/direct/dresources/permissions.go @@ -32,9 +32,17 @@ type ResourcePermissions struct { client *databricks.WorkspaceClient } +// StatePermission represents a permission entry in deployment state. +type StatePermission struct { + Level iam.PermissionLevel `json:"level,omitempty"` + UserName string `json:"user_name,omitempty"` + ServicePrincipalName string `json:"service_principal_name,omitempty"` + GroupName string `json:"group_name,omitempty"` +} + type PermissionsState struct { - ObjectID string `json:"object_id"` - Permissions []iam.AccessControlRequest `json:"permissions,omitempty"` + ObjectID string `json:"object_id"` + EmbeddedSlice []StatePermission `json:"_,omitempty"` } func PreparePermissionsInputConfig(inputConfig any, node string) (*structvar.StructVar, error) { @@ -54,7 +62,7 @@ func PreparePermissionsInputConfig(inputConfig any, node string) (*structvar.Str return nil, fmt.Errorf("unsupported permissions resource type: %s", resourceType) } - permissions, err := toAccessControlRequests(inputConfig) + permissions, err := toStatePermissions(inputConfig) if err != nil { return nil, err } @@ -76,8 +84,8 @@ func PreparePermissionsInputConfig(inputConfig any, node string) (*structvar.Str return &structvar.StructVar{ Value: &PermissionsState{ - ObjectID: "", // Always a reference, defined in Refs below - Permissions: permissions, + ObjectID: "", // Always a reference, defined in Refs below + EmbeddedSlice: permissions, }, Refs: map[string]string{ "object_id": objectIdRef, @@ -93,9 +101,9 @@ func (*ResourcePermissions) PrepareState(s *PermissionsState) *PermissionsState return s } -// toAccessControlRequests converts any slice of permission structs to []iam.AccessControlRequest. -// All permission types share the same underlying struct layout (Level, UserName, ServicePrincipalName, GroupName). -func toAccessControlRequests(ps any) ([]iam.AccessControlRequest, error) { +// toStatePermissions converts any slice of typed permission structs (e.g. []JobPermission) +// to []StatePermission. All permission types share the same underlying struct layout. +func toStatePermissions(ps any) ([]StatePermission, error) { v := reflect.ValueOf(ps) if v.Kind() == reflect.Pointer { v = v.Elem() @@ -103,21 +111,20 @@ func toAccessControlRequests(ps any) ([]iam.AccessControlRequest, error) { if v.Kind() != reflect.Slice { return nil, fmt.Errorf("expected permissions slice, got %T", ps) } - result := make([]iam.AccessControlRequest, v.Len()) + result := make([]StatePermission, v.Len()) for i := range v.Len() { elem := v.Index(i) - result[i] = iam.AccessControlRequest{ - PermissionLevel: iam.PermissionLevel(elem.FieldByName("Level").String()), + result[i] = StatePermission{ + Level: iam.PermissionLevel(elem.FieldByName("Level").String()), UserName: elem.FieldByName("UserName").String(), ServicePrincipalName: elem.FieldByName("ServicePrincipalName").String(), GroupName: elem.FieldByName("GroupName").String(), - ForceSendFields: nil, } } return result, nil } -func accessControlRequestKey(x iam.AccessControlRequest) (string, string) { +func permissionKey(x StatePermission) (string, string) { if x.UserName != "" { return "user_name", x.UserName } @@ -131,8 +138,10 @@ func accessControlRequestKey(x iam.AccessControlRequest) (string, string) { } func (*ResourcePermissions) KeyedSlices() map[string]any { + // Empty key because EmbeddedSlice appears at the root path of + // PermissionsState (no "permissions" prefix in struct walker paths). return map[string]any{ - "permissions": accessControlRequestKey, + "": permissionKey, } } @@ -172,8 +181,8 @@ func (r *ResourcePermissions) DoRead(ctx context.Context, id string) (*Permissio } result := PermissionsState{ - ObjectID: id, - Permissions: nil, + ObjectID: id, + EmbeddedSlice: nil, } for _, accessControl := range acl.AccessControlList { @@ -182,12 +191,11 @@ func (r *ResourcePermissions) DoRead(ctx context.Context, id string) (*Permissio if permission.Inherited { continue } - result.Permissions = append(result.Permissions, iam.AccessControlRequest{ + result.EmbeddedSlice = append(result.EmbeddedSlice, StatePermission{ + Level: permission.PermissionLevel, GroupName: accessControl.GroupName, UserName: accessControl.UserName, ServicePrincipalName: accessControl.ServicePrincipalName, - PermissionLevel: permission.PermissionLevel, - ForceSendFields: nil, }) } } @@ -213,10 +221,21 @@ func (r *ResourcePermissions) DoUpdate(ctx context.Context, _ string, newState * return nil, err } + acl := make([]iam.AccessControlRequest, len(newState.EmbeddedSlice)) + for i, p := range newState.EmbeddedSlice { + acl[i] = iam.AccessControlRequest{ + PermissionLevel: p.Level, + UserName: p.UserName, + ServicePrincipalName: p.ServicePrincipalName, + GroupName: p.GroupName, + ForceSendFields: nil, + } + } + _, err = r.client.Permissions.Set(ctx, iam.SetObjectPermissions{ RequestObjectId: extractedID, RequestObjectType: extractedType, - AccessControlList: newState.Permissions, + AccessControlList: acl, }) return nil, err diff --git a/bundle/direct/dstate/migrate.go b/bundle/direct/dstate/migrate.go new file mode 100644 index 0000000000..ebbd167699 --- /dev/null +++ b/bundle/direct/dstate/migrate.go @@ -0,0 +1,98 @@ +package dstate + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/databricks/cli/bundle/direct/dresources" + "github.com/databricks/databricks-sdk-go/service/iam" +) + +// migrateState runs all necessary migrations on the database. +// It is called after loading state from disk. +func migrateState(db *Database) error { + if db.StateVersion >= currentStateVersion { + return nil + } + + for version := db.StateVersion; version < currentStateVersion; version++ { + fn, ok := migrations[version] + if !ok { + return fmt.Errorf("unsupported state version %d (current: %d)", version, currentStateVersion) + } + if err := fn(db); err != nil { + return fmt.Errorf("migrating state from version %d: %w", version, err) + } + db.StateVersion = version + 1 + } + + return nil +} + +// migrations maps source version to the function that migrates to version+1. +// Version 0 means state_version was absent in old state files; treat same as 1. +var migrations = map[int]func(*Database) error{ + 0: migrateV1ToV2, + 1: migrateV1ToV2, +} + +// migrateV1ToV2 migrates permissions entries from the old format +// (iam.AccessControlRequest with "permissions" key and "permission_level" field) +// to the new format (StatePermission with "_" key and "level" field). +func migrateV1ToV2(db *Database) error { + for key, entry := range db.State { + if !strings.HasSuffix(key, ".permissions") { + continue + } + if len(entry.State) == 0 { + continue + } + migrated, err := migratePermissionsEntry(entry.State) + if err != nil { + return fmt.Errorf("migrating %s: %w", key, err) + } + entry.State = migrated + db.State[key] = entry + } + return nil +} + +// Old types from main branch — copied here for migration purposes. +type oldPermissionsStateV1 struct { + ObjectID string `json:"object_id"` + Permissions []oldPermissionV1 `json:"permissions,omitempty"` +} + +type oldPermissionV1 struct { + PermissionLevel string `json:"permission_level,omitempty"` + UserName string `json:"user_name,omitempty"` + ServicePrincipalName string `json:"service_principal_name,omitempty"` + GroupName string `json:"group_name,omitempty"` +} + +func migratePermissionsEntry(raw json.RawMessage) (json.RawMessage, error) { + var old oldPermissionsStateV1 + if err := json.Unmarshal(raw, &old); err != nil { + return nil, err + } + + // If old format had no permissions, try parsing as new format (might already be migrated). + if len(old.Permissions) == 0 { + return raw, nil + } + + newState := dresources.PermissionsState{ + ObjectID: old.ObjectID, + } + for _, p := range old.Permissions { + newState.EmbeddedSlice = append(newState.EmbeddedSlice, dresources.StatePermission{ + Level: iam.PermissionLevel(p.PermissionLevel), + UserName: p.UserName, + ServicePrincipalName: p.ServicePrincipalName, + GroupName: p.GroupName, + }) + } + + return json.MarshalIndent(newState, " ", " ") +} diff --git a/bundle/direct/dstate/migrate_test.go b/bundle/direct/dstate/migrate_test.go new file mode 100644 index 0000000000..3b6d9c180f --- /dev/null +++ b/bundle/direct/dstate/migrate_test.go @@ -0,0 +1,107 @@ +package dstate + +import ( + "encoding/json" + "testing" + + "github.com/databricks/cli/bundle/direct/dresources" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMigrateV1ToV2_PermissionsEntry(t *testing.T) { + db := Database{ + StateVersion: 1, + CLIVersion: "0.0.0-dev", + Lineage: "test", + Serial: 1, + State: map[string]ResourceEntry{ + "resources.jobs.my_job": { + ID: "123", + State: json.RawMessage(`{"name": "my job"}`), + }, + "resources.jobs.my_job.permissions": { + ID: "/jobs/123", + State: json.RawMessage(`{ + "object_id": "/jobs/123", + "permissions": [ + {"permission_level": "CAN_VIEW", "group_name": "viewers"}, + {"permission_level": "IS_OWNER", "user_name": "tester@databricks.com"} + ] + }`), + }, + }, + } + + err := migrateState(&db) + require.NoError(t, err) + assert.Equal(t, 2, db.StateVersion) + + // Non-permissions entry should be unchanged. + assert.Equal(t, `{"name": "my job"}`, string(db.State["resources.jobs.my_job"].State)) + + // Permissions entry should be migrated. + var result dresources.PermissionsState + err = json.Unmarshal(db.State["resources.jobs.my_job.permissions"].State, &result) + require.NoError(t, err) + assert.Equal(t, "/jobs/123", result.ObjectID) + require.Len(t, result.EmbeddedSlice, 2) + assert.Equal(t, "CAN_VIEW", string(result.EmbeddedSlice[0].Level)) + assert.Equal(t, "viewers", result.EmbeddedSlice[0].GroupName) + assert.Equal(t, "IS_OWNER", string(result.EmbeddedSlice[1].Level)) + assert.Equal(t, "tester@databricks.com", result.EmbeddedSlice[1].UserName) +} + +func TestMigrateV1ToV2_AlreadyNewFormat(t *testing.T) { + // State that already uses new format (e.g., was created by new CLI but version wasn't bumped). + db := Database{ + StateVersion: 1, + CLIVersion: "0.0.0-dev", + Lineage: "test", + Serial: 1, + State: map[string]ResourceEntry{ + "resources.jobs.my_job.permissions": { + ID: "/jobs/123", + State: json.RawMessage(`{ + "object_id": "/jobs/123", + "_": [ + {"level": "CAN_VIEW", "group_name": "viewers"} + ] + }`), + }, + }, + } + + err := migrateState(&db) + require.NoError(t, err) + assert.Equal(t, 2, db.StateVersion) + + // Should pass through unchanged (old.Permissions is empty, so raw is returned as-is). + var result dresources.PermissionsState + err = json.Unmarshal(db.State["resources.jobs.my_job.permissions"].State, &result) + require.NoError(t, err) + assert.Equal(t, "CAN_VIEW", string(result.EmbeddedSlice[0].Level)) +} + +func TestMigrateState_CurrentVersion(t *testing.T) { + db := Database{ + StateVersion: currentStateVersion, + State: map[string]ResourceEntry{}, + } + + err := migrateState(&db) + require.NoError(t, err) + assert.Equal(t, currentStateVersion, db.StateVersion) +} + +func TestMigrateState_Version0(t *testing.T) { + // Version 0 means state_version was absent; should be treated like version 1. + db := Database{ + StateVersion: 0, + State: map[string]ResourceEntry{}, + } + + err := migrateState(&db) + require.NoError(t, err) + assert.Equal(t, currentStateVersion, db.StateVersion) +} diff --git a/bundle/direct/dstate/state.go b/bundle/direct/dstate/state.go index c105e9c49c..e1c776b479 100644 --- a/bundle/direct/dstate/state.go +++ b/bundle/direct/dstate/state.go @@ -15,7 +15,7 @@ import ( "github.com/google/uuid" ) -const currentStateVersion = 1 +const currentStateVersion = 2 type DeploymentState struct { Path string @@ -124,6 +124,10 @@ func (db *DeploymentState) Open(path string) error { return err } + if err := migrateState(&db.Data); err != nil { + return fmt.Errorf("migrating state %s: %w", path, err) + } + db.Path = path return nil } diff --git a/cmd/bundle/debug/refschema.go b/cmd/bundle/debug/refschema.go index 932b8d62ab..3c7e90b3bc 100644 --- a/cmd/bundle/debug/refschema.go +++ b/cmd/bundle/debug/refschema.go @@ -100,7 +100,11 @@ func dumpRemoteSchemas(out io.Writer) error { byType := pathTypes[p] for _, t := range utils.SortedKeys(byType) { info := formatTags(byType[t]) - lines = append(lines, fmt.Sprintf("%s.%s\t%s\t%s\n", resourcePrefix, p, t, info)) + sep := "." + if strings.HasPrefix(p, "[") { + sep = "" + } + lines = append(lines, fmt.Sprintf("%s%s%s\t%s\t%s\n", resourcePrefix, sep, p, t, info)) } } diff --git a/libs/structs/structaccess/embed.go b/libs/structs/structaccess/embed.go new file mode 100644 index 0000000000..39a3eec60c --- /dev/null +++ b/libs/structs/structaccess/embed.go @@ -0,0 +1,50 @@ +package structaccess + +import ( + "reflect" +) + +// EmbeddedSliceFieldName is the Go field name that signals struct walkers to treat +// the field as transparent — its contents appear directly at the parent path level +// without adding the field name to the path. Only supported on slice fields with +// one EmbeddedSlice field per struct. +const EmbeddedSliceFieldName = "EmbeddedSlice" + +// findEmbedField returns the value of the EmbeddedSlice field in struct v, if any. +// Returns an invalid reflect.Value if no EmbeddedSlice field exists. +func findEmbedField(v reflect.Value) reflect.Value { + if v.Kind() != reflect.Struct { + return reflect.Value{} + } + t := v.Type() + for i := range t.NumField() { + sf := t.Field(i) + if sf.PkgPath != "" { + continue + } + if sf.Name == EmbeddedSliceFieldName { + return v.Field(i) + } + } + return reflect.Value{} +} + +// findEmbedFieldType returns the type of the EmbeddedSlice field in struct type t, if any. +func findEmbedFieldType(t reflect.Type) reflect.Type { + for t.Kind() == reflect.Pointer { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return nil + } + for i := range t.NumField() { + sf := t.Field(i) + if sf.PkgPath != "" { + continue + } + if sf.Name == EmbeddedSliceFieldName { + return sf.Type + } + } + return nil +} diff --git a/libs/structs/structaccess/get.go b/libs/structs/structaccess/get.go index f9b6b70baf..c843554065 100644 --- a/libs/structs/structaccess/get.go +++ b/libs/structs/structaccess/get.go @@ -57,6 +57,12 @@ func getValue(v any, path *structpath.PathNode) (reflect.Value, error) { } if idx, isIndex := node.Index(); isIndex { + // If cur is a struct with an EmbeddedSlice field, navigate through it. + if cur.Kind() == reflect.Struct { + if embed := findEmbedField(cur); embed.IsValid() { + cur = embed + } + } kind := cur.Kind() if kind != reflect.Slice && kind != reflect.Array { return reflect.Value{}, fmt.Errorf("%s: cannot index %s", node.String(), kind) @@ -69,6 +75,12 @@ func getValue(v any, path *structpath.PathNode) (reflect.Value, error) { } if key, value, ok := node.KeyValue(); ok { + // If cur is a struct with an EmbeddedSlice field, navigate through it. + if cur.Kind() == reflect.Struct { + if embed := findEmbedField(cur); embed.IsValid() { + cur = embed + } + } nv, err := accessKeyValue(cur, key, value, node) if err != nil { return reflect.Value{}, err @@ -234,6 +246,9 @@ func findFieldInStruct(v reflect.Value, key string) (reflect.Value, reflect.Stru name = "" } + if sf.Name == EmbeddedSliceFieldName { + continue // EmbeddedSlice fields are not accessible by name + } if name != "" && name == key { // Skip fields marked as internal or readonly via bundle tag btag := structtag.BundleTag(sf.Tag.Get("bundle")) diff --git a/libs/structs/structaccess/get_test.go b/libs/structs/structaccess/get_test.go index cfc3365b75..f498b4a8de 100644 --- a/libs/structs/structaccess/get_test.go +++ b/libs/structs/structaccess/get_test.go @@ -738,6 +738,82 @@ func TestPipeline(t *testing.T) { require.Nil(t, v) } +func TestGet_EmbedTag(t *testing.T) { + type Item struct { + Name string `json:"name"` + Level string `json:"level,omitempty"` + } + + type Container struct { + ObjectID string `json:"object_id"` + EmbeddedSlice []Item `json:"items,omitempty"` + } + + c := Container{ + ObjectID: "abc", + EmbeddedSlice: []Item{ + {Name: "alice", Level: "admin"}, + {Name: "bob", Level: "reader"}, + }, + } + + // Access non-embed field normally. + testGet(t, c, "object_id", "abc") + + // Access EmbeddedSlice elements via index. + testGet(t, c, "[0].name", "alice") + testGet(t, c, "[1].level", "reader") + + // Access via key-value selector. + testGet(t, c, "[name='bob'].level", "reader") + + // Out of range. + _, err := GetByString(c, "[5].name") + require.Error(t, err) + var notFound *NotFoundError + require.ErrorAs(t, err, ¬Found) + + // EmbeddedSlice field is not accessible by json tag name. + _, err = GetByString(c, "items") + require.Error(t, err) + require.NotErrorAs(t, err, ¬Found) +} + +func TestValidate_EmbedTag(t *testing.T) { + type Item struct { + Name string `json:"name"` + Level string `json:"level,omitempty"` + } + + type Container struct { + ObjectID string `json:"object_id"` + EmbeddedSlice []Item `json:"items,omitempty"` + } + + typ := reflect.TypeOf(Container{}) + + // Valid paths through EmbeddedSlice. + require.NoError(t, ValidateByString(typ, "[0].name")) + require.NoError(t, ValidateByString(typ, "[*].level")) + require.NoError(t, ValidateByString(typ, "[name='alice'].level")) + require.NoError(t, ValidateByString(typ, "object_id")) + + // EmbeddedSlice itself is not accessible by json tag name. + require.Error(t, ValidateByString(typ, "items")) + require.Error(t, ValidateByString(typ, "items[0].name")) +} + +func TestGet_EmbedTagEmpty(t *testing.T) { + type Container struct { + ObjectID string `json:"object_id"` + EmbeddedSlice []int `json:"items,omitempty"` + } + + // Empty embed slice with omitempty: index should fail. + _, err := GetByString(Container{ObjectID: "abc"}, "[0]") + require.Error(t, err) +} + func TestGetKeyValue_NestedMultiple(t *testing.T) { type Item struct { ID string `json:"id"` diff --git a/libs/structs/structaccess/set.go b/libs/structs/structaccess/set.go index 7f12a77751..2285d470fa 100644 --- a/libs/structs/structaccess/set.go +++ b/libs/structs/structaccess/set.go @@ -78,12 +78,19 @@ func setValueAtNode(parentVal reflect.Value, node *structpath.PathNode, value an valueVal := reflect.ValueOf(value) if idx, isIndex := node.Index(); isIndex { + // If parent is a struct with an EmbeddedSlice field, navigate through it. + if parentVal.Kind() == reflect.Struct { + if embed := findEmbedField(parentVal); embed.IsValid() { + parentVal = embed + } + } return setArrayElement(parentVal, idx, valueVal) } // Note: wildcards cannot appear in PathNode (Parse rejects them) if key, matchValue, isKeyValue := node.KeyValue(); isKeyValue { + // Note: EmbeddedSlice doesn't apply here since key-value selectors can't be set targets. return fmt.Errorf("cannot set value at key-value selector [%s='%s'] - key-value syntax can only be used for path traversal, not as a final target", key, matchValue) } diff --git a/libs/structs/structaccess/set_test.go b/libs/structs/structaccess/set_test.go index 25674c1211..3b292e70fe 100644 --- a/libs/structs/structaccess/set_test.go +++ b/libs/structs/structaccess/set_test.go @@ -518,6 +518,41 @@ func testSetGet(t *testing.T, obj any, path string, setValue, expectedGetValue a require.Equal(t, expectedGetValue, got, "SetByString(%#v, %q, %#v) then GetByString should return %#v", obj, path, setValue, expectedGetValue) } +func TestSet_EmbedTag(t *testing.T) { + type Item struct { + Name string `json:"name"` + Level string `json:"level,omitempty"` + } + + type Container struct { + ObjectID string `json:"object_id"` + EmbeddedSlice []Item `json:"items,omitempty"` + } + + c := &Container{ + ObjectID: "abc", + EmbeddedSlice: []Item{ + {Name: "alice", Level: "admin"}, + {Name: "bob", Level: "reader"}, + }, + } + + // Set a field via EmbeddedSlice index. + err := structaccess.SetByString(c, "[0].level", "writer") + require.NoError(t, err) + assert.Equal(t, "writer", c.EmbeddedSlice[0].Level) + + // Set non-embed field normally. + err = structaccess.SetByString(c, "object_id", "def") + require.NoError(t, err) + assert.Equal(t, "def", c.ObjectID) + + // Set via key-value path traversal. + err = structaccess.SetByString(c, "[name='bob'].level", "admin") + require.NoError(t, err) + assert.Equal(t, "admin", c.EmbeddedSlice[1].Level) +} + func TestSetJobSettings(t *testing.T) { jobSettings := jobs.JobSettings{ Name: "job foo", diff --git a/libs/structs/structaccess/typecheck.go b/libs/structs/structaccess/typecheck.go index d2f8ed2581..2fa9a1f8d4 100644 --- a/libs/structs/structaccess/typecheck.go +++ b/libs/structs/structaccess/typecheck.go @@ -55,6 +55,11 @@ func validateNodeSlice(t reflect.Type, nodes []*structpath.PatternNode) error { // Index access: slice/array if _, isIndex := node.Index(); isIndex { + if cur.Kind() == reflect.Struct { + if embedType := findEmbedFieldType(cur); embedType != nil { + cur = embedType + } + } kind := cur.Kind() if kind != reflect.Slice && kind != reflect.Array { return fmt.Errorf("%s: cannot index %s", node.String(), kind) @@ -65,6 +70,11 @@ func validateNodeSlice(t reflect.Type, nodes []*structpath.PatternNode) error { // Handle wildcards - treat like index/key access if node.BracketStar() { + if cur.Kind() == reflect.Struct { + if embedType := findEmbedFieldType(cur); embedType != nil { + cur = embedType + } + } kind := cur.Kind() if kind != reflect.Slice && kind != reflect.Array { return fmt.Errorf("%s: cannot use [*] on %s", node.String(), kind) @@ -82,6 +92,11 @@ func validateNodeSlice(t reflect.Type, nodes []*structpath.PatternNode) error { // Handle key-value selector: validates that we can index the slice/array if _, _, isKeyValue := node.KeyValue(); isKeyValue { + if cur.Kind() == reflect.Struct { + if embedType := findEmbedFieldType(cur); embedType != nil { + cur = embedType + } + } kind := cur.Kind() if kind != reflect.Slice && kind != reflect.Array { return fmt.Errorf("%s: cannot use key-value syntax on %s", node.String(), kind) @@ -133,7 +148,7 @@ func FindStructFieldByKeyType(t reflect.Type, key string) (reflect.StructField, continue } name := structtag.JSONTag(sf.Tag.Get("json")).Name() - if name == "-" { + if name == "-" || sf.Name == EmbeddedSliceFieldName { name = "" } if name != "" && name == key { diff --git a/libs/structs/structdiff/diff.go b/libs/structs/structdiff/diff.go index 886f9c1f83..a34356e167 100644 --- a/libs/structs/structdiff/diff.go +++ b/libs/structs/structdiff/diff.go @@ -7,6 +7,7 @@ import ( "sort" "strings" + "github.com/databricks/cli/libs/structs/structaccess" "github.com/databricks/cli/libs/structs/structpath" "github.com/databricks/cli/libs/structs/structtag" ) @@ -206,10 +207,11 @@ func diffStruct(ctx *diffContext, path *structpath.PathNode, s1, s2 reflect.Valu continue } - if fieldName == "" { + isEmbed := sf.Name == structaccess.EmbeddedSliceFieldName + + if fieldName == "" || isEmbed { fieldName = sf.Name } - node := structpath.NewDotString(path, fieldName) v1Field := s1.Field(i) v2Field := s2.Field(i) @@ -232,6 +234,14 @@ func diffStruct(ctx *diffContext, path *structpath.PathNode, s1, s2 reflect.Valu } } + // EmbeddedSlice: diff at parent path level without adding field name. + var node *structpath.PathNode + if isEmbed { + node = path + } else { + node = structpath.NewDotString(path, fieldName) + } + if err := diffValues(ctx, node, v1Field, v2Field, changes); err != nil { return err } diff --git a/libs/structs/structdiff/diff_test.go b/libs/structs/structdiff/diff_test.go index 9e12be527e..4228a148cf 100644 --- a/libs/structs/structdiff/diff_test.go +++ b/libs/structs/structdiff/diff_test.go @@ -450,6 +450,114 @@ func TestGetStructDiff(t *testing.T) { } } +type EmbedItem struct { + Name string `json:"name,omitempty"` + Level string `json:"level,omitempty"` +} + +type EmbedContainer struct { + ObjectID string `json:"object_id"` + EmbeddedSlice []EmbedItem `json:"items,omitempty"` +} + +func embedItemKey(item EmbedItem) (string, string) { + return "name", item.Name +} + +func TestGetStructDiffEmbedTag(t *testing.T) { + tests := []struct { + name string + a, b EmbedContainer + want []ResolvedChange + }{ + { + name: "no changes", + a: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice", Level: "admin"}}}, + b: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice", Level: "admin"}}}, + want: nil, + }, + { + name: "embed field change without keys", + a: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice", Level: "admin"}}}, + b: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice", Level: "reader"}}}, + want: []ResolvedChange{{Field: "[0].level", Old: "admin", New: "reader"}}, + }, + { + name: "embed element added", + a: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice"}}}, + b: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice"}, {Name: "bob"}}}, + // Different lengths without key func → whole-slice change + want: []ResolvedChange{{Field: "", Old: []EmbedItem{{Name: "alice"}}, New: []EmbedItem{{Name: "alice"}, {Name: "bob"}}}}, + }, + { + name: "non-embed field change", + a: EmbedContainer{ObjectID: "abc"}, + b: EmbedContainer{ObjectID: "def"}, + want: []ResolvedChange{{Field: "object_id", Old: "abc", New: "def"}}, + }, + { + name: "embed slice empty vs non-empty", + a: EmbedContainer{ObjectID: "abc"}, + b: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice"}}}, + want: []ResolvedChange{{Field: "", Old: nil, New: []EmbedItem{{Name: "alice"}}}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetStructDiff(tt.a, tt.b, nil) + assert.NoError(t, err) + assert.Equal(t, tt.want, resolveChanges(got)) + }) + } +} + +func TestGetStructDiffEmbedTagWithKeyFunc(t *testing.T) { + // The EmbeddedSlice field appears at root path, so key pattern is "". + sliceKeys := map[string]KeyFunc{ + "": embedItemKey, + } + + tests := []struct { + name string + a, b EmbedContainer + want []ResolvedChange + }{ + { + name: "reorder with key func", + a: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice", Level: "admin"}, {Name: "bob", Level: "reader"}}}, + b: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "bob", Level: "reader"}, {Name: "alice", Level: "admin"}}}, + want: nil, + }, + { + name: "field change with key func", + a: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice", Level: "admin"}}}, + b: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice", Level: "reader"}}}, + want: []ResolvedChange{{Field: "[name='alice'].level", Old: "admin", New: "reader"}}, + }, + { + name: "element added with key func", + a: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice"}}}, + b: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice"}, {Name: "bob", Level: "reader"}}}, + want: []ResolvedChange{{Field: "[name='bob']", Old: nil, New: EmbedItem{Name: "bob", Level: "reader"}}}, + }, + { + name: "element removed with key func", + a: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice", Level: "admin"}, {Name: "bob"}}}, + b: EmbedContainer{ObjectID: "abc", EmbeddedSlice: []EmbedItem{{Name: "alice", Level: "admin"}}}, + want: []ResolvedChange{{Field: "[name='bob']", Old: EmbedItem{Name: "bob"}, New: nil}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetStructDiff(tt.a, tt.b, sliceKeys) + assert.NoError(t, err) + assert.Equal(t, tt.want, resolveChanges(got)) + }) + } +} + type Task struct { TaskKey string `json:"task_key,omitempty"` Description string `json:"description,omitempty"` diff --git a/libs/structs/structtag/jsontag_test.go b/libs/structs/structtag/jsontag_test.go index b45fd566ea..b5a22a72b4 100644 --- a/libs/structs/structtag/jsontag_test.go +++ b/libs/structs/structtag/jsontag_test.go @@ -35,17 +35,14 @@ func TestJSONTagMethods(t *testing.T) { for _, tt := range tests { tag := JSONTag(tt.tag) - // Test Name method if gotName := tag.Name(); gotName != tt.wantName { t.Errorf("JSONTag(%q).Name() = %q; want %q", tt.tag, gotName, tt.wantName) } - // Test OmitEmpty method if gotOmitEmpty := tag.OmitEmpty(); gotOmitEmpty != tt.wantOmitempty { t.Errorf("JSONTag(%q).OmitEmpty() = %v; want %v", tt.tag, gotOmitEmpty, tt.wantOmitempty) } - // Test OmitZero method if gotOmitZero := tag.OmitZero(); gotOmitZero != tt.wantOmitzero { t.Errorf("JSONTag(%q).OmitZero() = %v; want %v", tt.tag, gotOmitZero, tt.wantOmitzero) } diff --git a/libs/structs/structwalk/walk.go b/libs/structs/structwalk/walk.go index 791077cfc6..d5430855fc 100644 --- a/libs/structs/structwalk/walk.go +++ b/libs/structs/structwalk/walk.go @@ -6,6 +6,7 @@ import ( "slices" "sort" + "github.com/databricks/cli/libs/structs/structaccess" "github.com/databricks/cli/libs/structs/structpath" "github.com/databricks/cli/libs/structs/structtag" ) @@ -131,7 +132,6 @@ func walkStruct(path *structpath.PathNode, s reflect.Value, visit VisitFunc) { if fieldName == "" { fieldName = sf.Name } - node := structpath.NewDotString(path, fieldName) fieldVal := s.Field(i) // Skip zero values with omitempty unless field is explicitly forced. @@ -139,6 +139,13 @@ func walkStruct(path *structpath.PathNode, s reflect.Value, visit VisitFunc) { continue } + // EmbeddedSlice: walk directly without adding the field name to the path. + if sf.Name == structaccess.EmbeddedSliceFieldName { + walkValue(path, fieldVal, &sf, visit) + continue + } + + node := structpath.NewDotString(path, fieldName) walkValue(node, fieldVal, &sf, visit) } } diff --git a/libs/structs/structwalk/walk_test.go b/libs/structs/structwalk/walk_test.go index d754329b6d..57a7c96d50 100644 --- a/libs/structs/structwalk/walk_test.go +++ b/libs/structs/structwalk/walk_test.go @@ -216,6 +216,42 @@ func TestEmbeddedStructWithPointer(t *testing.T) { }, flatten(t, parent)) } +func TestEmbedTagWalk(t *testing.T) { + type Item struct { + Name string `json:"name"` + } + + type Container struct { + ObjectID string `json:"object_id"` + EmbeddedSlice []Item `json:"items,omitempty"` + } + + c := Container{ + ObjectID: "abc", + EmbeddedSlice: []Item{{Name: "first"}, {Name: "second"}}, + } + + result := flatten(t, c) + + // EmbeddedSlice field contents appear at parent level without the field name. + assert.Equal(t, map[string]any{ + "object_id": "abc", + "[0].name": "first", + "[1].name": "second", + }, result) +} + +func TestEmbedTagWalkEmpty(t *testing.T) { + type Container struct { + ObjectID string `json:"object_id"` + EmbeddedSlice []int `json:"items,omitempty"` + } + + // Empty slice with omitempty should be skipped. + result := flatten(t, Container{ObjectID: "abc"}) + assert.Equal(t, map[string]any{"object_id": "abc"}, result) +} + func TestEmbeddedStructWithJSONTagDash(t *testing.T) { type Embedded struct { SkipField string `json:"-"` diff --git a/libs/structs/structwalk/walktype.go b/libs/structs/structwalk/walktype.go index 604e3e5c4a..8e67a0d44f 100644 --- a/libs/structs/structwalk/walktype.go +++ b/libs/structs/structwalk/walktype.go @@ -4,6 +4,7 @@ import ( "errors" "reflect" + "github.com/databricks/cli/libs/structs/structaccess" "github.com/databricks/cli/libs/structs/structpath" "github.com/databricks/cli/libs/structs/structtag" ) @@ -119,11 +120,18 @@ func walkTypeStruct(path *structpath.PatternNode, st reflect.Type, visit VisitTy } // Skip fields marked as "-" in json tag - jsonTagName := structtag.JSONTag(jsonTag).Name() + jsonTagParsed := structtag.JSONTag(jsonTag) + jsonTagName := jsonTagParsed.Name() if jsonTagName == "-" { continue } + // EmbeddedSlice: walk at parent path level without adding field name. + if sf.Name == structaccess.EmbeddedSliceFieldName { + walkTypeValue(path, sf.Type, &sf, visit, visitedCount) + continue + } + // Resolve field name from JSON tag or fall back to Go field name fieldName := jsonTagName if fieldName == "" { diff --git a/libs/structs/structwalk/walktype_test.go b/libs/structs/structwalk/walktype_test.go index dd452e8d3e..c2e2940adb 100644 --- a/libs/structs/structwalk/walktype_test.go +++ b/libs/structs/structwalk/walktype_test.go @@ -225,6 +225,34 @@ func TestTypeBundleTag(t *testing.T) { assert.Equal(t, []string{"B", "D"}, internal) } +func TestWalkTypeEmbedTag(t *testing.T) { + type Item struct { + Name string `json:"name"` + } + + type Container struct { + ObjectID string `json:"object_id"` + EmbeddedSlice []Item `json:"items,omitempty"` + } + + var visited []string + err := WalkType(reflect.TypeOf(Container{}), func(path *structpath.PatternNode, typ reflect.Type, field *reflect.StructField) (continueWalk bool) { + if path == nil { + return true + } + visited = append(visited, path.String()) + return true + }) + require.NoError(t, err) + + // EmbeddedSlice field should not appear as "items" but its contents should be at parent level. + assert.Equal(t, []string{ + "object_id", + "[*]", + "[*].name", + }, visited) +} + func TestWalkTypeVisited(t *testing.T) { type Inner struct { A int