diff --git a/.nextchanges/bundles/grants-empty-list-noop.md b/.nextchanges/bundles/grants-empty-list-noop.md new file mode 100644 index 0000000000..3ba39fd6ca --- /dev/null +++ b/.nextchanges/bundles/grants-empty-list-noop.md @@ -0,0 +1 @@ +The direct deployment engine no longer emits a no-op grants PATCH when a resource is created with an empty `grants: []` and no prior state, matching Terraform. Removing previously-granted principals by setting `grants: []` still works. diff --git a/.nextchanges/bundles/permissions-empty-list.md b/.nextchanges/bundles/permissions-empty-list.md new file mode 100644 index 0000000000..a42cb5eb14 --- /dev/null +++ b/.nextchanges/bundles/permissions-empty-list.md @@ -0,0 +1 @@ +An explicit `permissions: []` on a resource is now managed like a non-empty list: the current user is added as the default owner (`IS_OWNER`), matching Terraform. Previously an empty list was treated as if the block were absent. diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/databricks.yml.tmpl b/acceptance/bundle/resources/grants/schemas/empty_list/databricks.yml.tmpl new file mode 100644 index 0000000000..ba68e8b85f --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list/databricks.yml.tmpl @@ -0,0 +1,12 @@ +bundle: + name: schema-grants-empty-list-$UNIQUE_NAME + +resources: + schemas: + grants_schema: + name: schema_grants_empty_list_$UNIQUE_NAME + catalog_name: main + # Unlike permissions, grants have no default owner: an empty list grants + # nothing. Note the engines diverge on create: direct emits a no-op PATCH + # with an empty body, terraform emits no request at all. + grants: [] diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/databricks_populated.yml.tmpl b/acceptance/bundle/resources/grants/schemas/empty_list/databricks_populated.yml.tmpl new file mode 100644 index 0000000000..111c501c05 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list/databricks_populated.yml.tmpl @@ -0,0 +1,12 @@ +bundle: + name: schema-grants-empty-list-$UNIQUE_NAME + +resources: + schemas: + grants_schema: + name: schema_grants_empty_list_$UNIQUE_NAME + catalog_name: main + grants: + - principal: $CURRENT_USER_NAME + privileges: + - CREATE_TABLE diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/out.plan.direct.txt b/acceptance/bundle/resources/grants/schemas/empty_list/out.plan.direct.txt new file mode 100644 index 0000000000..aea7bf49a7 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list/out.plan.direct.txt @@ -0,0 +1,15 @@ +{ + "plan_version": 2, + "cli_version": "[CLI_VERSION]", + "plan": { + "resources.schemas.grants_schema": { + "action": "create", + "new_state": { + "value": { + "catalog_name": "main", + "name": "schema_grants_empty_list_[UNIQUE_NAME]" + } + } + } + } +} diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/out.plan.terraform.txt b/acceptance/bundle/resources/grants/schemas/empty_list/out.plan.terraform.txt new file mode 100644 index 0000000000..f0c5799480 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list/out.plan.terraform.txt @@ -0,0 +1,8 @@ +{ + "cli_version": "[CLI_VERSION]", + "plan": { + "resources.schemas.grants_schema": { + "action": "create" + } + } +} diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.deploy.direct.json b/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.deploy.direct.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.deploy.terraform.json b/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.deploy.terraform.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.grant.direct.json b/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.grant.direct.json new file mode 100644 index 0000000000..e493713058 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.grant.direct.json @@ -0,0 +1,17 @@ +{ + "method": "PATCH", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_grants_empty_list_[UNIQUE_NAME]", + "body": { + "changes": [ + { + "add": [ + "CREATE_TABLE" + ], + "principal": "[USERNAME]", + "remove": [ + "ALL_PRIVILEGES" + ] + } + ] + } +} diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.grant.terraform.json b/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.grant.terraform.json new file mode 100644 index 0000000000..fa924388e4 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.grant.terraform.json @@ -0,0 +1,14 @@ +{ + "method": "PATCH", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_grants_empty_list_[UNIQUE_NAME]", + "body": { + "changes": [ + { + "add": [ + "CREATE_TABLE" + ], + "principal": "[USERNAME]" + } + ] + } +} diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.remove.direct.json b/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.remove.direct.json new file mode 100644 index 0000000000..ff4e782577 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.remove.direct.json @@ -0,0 +1,14 @@ +{ + "method": "PATCH", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_grants_empty_list_[UNIQUE_NAME]", + "body": { + "changes": [ + { + "principal": "[USERNAME]", + "remove": [ + "ALL_PRIVILEGES" + ] + } + ] + } +} diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.remove.terraform.json b/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.remove.terraform.json new file mode 100644 index 0000000000..a7ee26dfcb --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list/out.requests.remove.terraform.json @@ -0,0 +1,14 @@ +{ + "method": "PATCH", + "path": "/api/2.1/unity-catalog/permissions/schema/main.schema_grants_empty_list_[UNIQUE_NAME]", + "body": { + "changes": [ + { + "principal": "[USERNAME]", + "remove": [ + "CREATE_TABLE" + ] + } + ] + } +} diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/out.test.toml b/acceptance/bundle/resources/grants/schemas/empty_list/out.test.toml new file mode 100644 index 0000000000..e849ec85ac --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list/out.test.toml @@ -0,0 +1,4 @@ +Local = true +Cloud = true +RequiresUnityCatalog = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/output.txt b/acceptance/bundle/resources/grants/schemas/empty_list/output.txt new file mode 100644 index 0000000000..e9663bcd89 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list/output.txt @@ -0,0 +1,35 @@ + +>>> [CLI] bundle validate -o json +[] + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/schema-grants-empty-list-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +=== Grant a principal, then remove all grants with grants: [] + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/schema-grants-empty-list-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/schema-grants-empty-list-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> errcode [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.schemas.grants_schema + +This action will result in the deletion of the following UC schemas. Any underlying data may be lost: + delete resources.schemas.grants_schema + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/schema-grants-empty-list-[UNIQUE_NAME]/default + +Deleting files... +Destroy complete! diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/script b/acceptance/bundle/resources/grants/schemas/empty_list/script new file mode 100644 index 0000000000..97c2f01df8 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list/script @@ -0,0 +1,28 @@ +envsubst < databricks.yml.tmpl > databricks.yml + +cleanup() { + trace errcode $CLI bundle destroy --auto-approve + rm -f out.requests.txt +} +trap cleanup EXIT + +trace $CLI bundle validate -o json | jq .resources.schemas.grants_schema.grants + +errcode $CLI bundle plan -o json &> out.plan.$DATABRICKS_BUNDLE_ENGINE.txt +rm -f out.requests.txt + +# Create with an empty list: nothing to grant and no prior state, so the direct +# engine emits no request (matching terraform), not a no-op PATCH. +trace $CLI bundle deploy +print_requests.py //permissions > out.requests.deploy.$DATABRICKS_BUNDLE_ENGINE.json + +title "Grant a principal, then remove all grants with grants: []\n" +envsubst < databricks_populated.yml.tmpl > databricks.yml +trace $CLI bundle deploy +print_requests.py //permissions > out.requests.grant.$DATABRICKS_BUNDLE_ENGINE.json + +# Back to an empty list, but now the node is in state: the removal must still be +# planned and the PATCH must remove the previously granted privileges. +envsubst < databricks.yml.tmpl > databricks.yml +trace $CLI bundle deploy +print_requests.py //permissions > out.requests.remove.$DATABRICKS_BUNDLE_ENGINE.json diff --git a/acceptance/bundle/resources/grants/schemas/empty_list/test.toml b/acceptance/bundle/resources/grants/schemas/empty_list/test.toml new file mode 100644 index 0000000000..ab196c758c --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list/test.toml @@ -0,0 +1 @@ +Timeout = "5m" diff --git a/acceptance/bundle/resources/permissions/jobs/empty_list/databricks.yml b/acceptance/bundle/resources/permissions/jobs/empty_list/databricks.yml index 79f03b68bd..56475c0d2e 100644 --- a/acceptance/bundle/resources/permissions/jobs/empty_list/databricks.yml +++ b/acceptance/bundle/resources/permissions/jobs/empty_list/databricks.yml @@ -10,5 +10,5 @@ resources: notebook_task: notebook_path: /Workspace/Users/user@example.com/notebook source: WORKSPACE - # this is interpreted the same as nil, but it's IMO a bug; it should do default IS_OWNER setting + # An explicit empty list is managed: FixPermissions adds the default IS_OWNER for the current user. permissions: [] diff --git a/acceptance/bundle/resources/permissions/jobs/empty_list/out.plan.direct.json b/acceptance/bundle/resources/permissions/jobs/empty_list/out.plan.direct.json index 0ec914589b..88e1e115a9 100644 --- a/acceptance/bundle/resources/permissions/jobs/empty_list/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/empty_list/out.plan.direct.json @@ -28,6 +28,29 @@ ] } } + }, + "resources.jobs.foo.permissions": { + "depends_on": [ + { + "node": "resources.jobs.foo", + "label": "${resources.jobs.foo.id}" + } + ], + "action": "create", + "new_state": { + "value": { + "object_id": "", + "__embed__": [ + { + "level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + }, + "vars": { + "object_id": "/jobs/${resources.jobs.foo.id}" + } + } } } } diff --git a/acceptance/bundle/resources/permissions/jobs/empty_list/out.plan.terraform.json b/acceptance/bundle/resources/permissions/jobs/empty_list/out.plan.terraform.json index 5a250db78d..0db00e350f 100644 --- a/acceptance/bundle/resources/permissions/jobs/empty_list/out.plan.terraform.json +++ b/acceptance/bundle/resources/permissions/jobs/empty_list/out.plan.terraform.json @@ -3,6 +3,9 @@ "plan": { "resources.jobs.foo": { "action": "create" + }, + "resources.jobs.foo.permissions": { + "action": "create" } } } diff --git a/acceptance/bundle/resources/permissions/jobs/empty_list/out.requests.deploy.direct.json b/acceptance/bundle/resources/permissions/jobs/empty_list/out.requests.deploy.direct.json index e69de29bb2..f7aa2dbaa0 100644 --- a/acceptance/bundle/resources/permissions/jobs/empty_list/out.requests.deploy.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/empty_list/out.requests.deploy.direct.json @@ -0,0 +1,12 @@ +{ + "method": "PUT", + "path": "/api/2.0/permissions/jobs/[NUMID]", + "body": { + "access_control_list": [ + { + "permission_level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + } +} diff --git a/acceptance/bundle/resources/permissions/jobs/empty_list/out.requests.deploy.terraform.json b/acceptance/bundle/resources/permissions/jobs/empty_list/out.requests.deploy.terraform.json index e69de29bb2..f7aa2dbaa0 100644 --- a/acceptance/bundle/resources/permissions/jobs/empty_list/out.requests.deploy.terraform.json +++ b/acceptance/bundle/resources/permissions/jobs/empty_list/out.requests.deploy.terraform.json @@ -0,0 +1,12 @@ +{ + "method": "PUT", + "path": "/api/2.0/permissions/jobs/[NUMID]", + "body": { + "access_control_list": [ + { + "permission_level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + } +} diff --git a/acceptance/bundle/resources/permissions/jobs/empty_list/out.requests.destroy.terraform.json b/acceptance/bundle/resources/permissions/jobs/empty_list/out.requests.destroy.terraform.json index e69de29bb2..f7aa2dbaa0 100644 --- a/acceptance/bundle/resources/permissions/jobs/empty_list/out.requests.destroy.terraform.json +++ b/acceptance/bundle/resources/permissions/jobs/empty_list/out.requests.destroy.terraform.json @@ -0,0 +1,12 @@ +{ + "method": "PUT", + "path": "/api/2.0/permissions/jobs/[NUMID]", + "body": { + "access_control_list": [ + { + "permission_level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + } +} diff --git a/acceptance/bundle/resources/permissions/jobs/empty_list/output.txt b/acceptance/bundle/resources/permissions/jobs/empty_list/output.txt index ec71aa04be..1e0de88140 100644 --- a/acceptance/bundle/resources/permissions/jobs/empty_list/output.txt +++ b/acceptance/bundle/resources/permissions/jobs/empty_list/output.txt @@ -1,6 +1,11 @@ >>> [CLI] bundle validate -o json -null +[ + { + "level": "IS_OWNER", + "user_name": "[USERNAME]" + } +] >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... 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 57ee99c53f..d19fda4235 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 @@ -78,7 +78,18 @@ "label": "${resources.jobs.job_with_permissions.id}" } ], - "action": "delete", + "action": "update", + "new_state": { + "value": { + "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", + "__embed__": [ + { + "level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + } + }, "remote_state": { "object_id": "/jobs/[JOB_WITH_PERMISSIONS_ID]", "__embed__": [ @@ -95,6 +106,30 @@ "user_name": "[USERNAME]" } ] + }, + "changes": { + "[group_name='data-team']": { + "action": "update", + "old": { + "level": "CAN_MANAGE", + "group_name": "data-team" + }, + "remote": { + "level": "CAN_MANAGE", + "group_name": "data-team" + } + }, + "[user_name='viewer@example.com']": { + "action": "update", + "old": { + "level": "CAN_VIEW", + "user_name": "viewer@example.com" + }, + "remote": { + "level": "CAN_VIEW", + "user_name": "viewer@example.com" + } + } } } } diff --git a/acceptance/bundle/resources/permissions/jobs/update/out.plan_set_empty.terraform.json b/acceptance/bundle/resources/permissions/jobs/update/out.plan_set_empty.terraform.json index dea726555e..cc4ceee277 100644 --- a/acceptance/bundle/resources/permissions/jobs/update/out.plan_set_empty.terraform.json +++ b/acceptance/bundle/resources/permissions/jobs/update/out.plan_set_empty.terraform.json @@ -5,7 +5,7 @@ "action": "skip" }, "resources.jobs.job_with_permissions.permissions": { - "action": "delete" + "action": "update" } } } diff --git a/acceptance/bundle/resources/permissions/jobs/update/out.requests_destroy.terraform.json b/acceptance/bundle/resources/permissions/jobs/update/out.requests_destroy.terraform.json index 73495bdc38..f66a05564c 100644 --- a/acceptance/bundle/resources/permissions/jobs/update/out.requests_destroy.terraform.json +++ b/acceptance/bundle/resources/permissions/jobs/update/out.requests_destroy.terraform.json @@ -1,3 +1,15 @@ +{ + "method": "PUT", + "path": "/api/2.0/permissions/jobs/[JOB_WITH_PERMISSIONS_ID]", + "body": { + "access_control_list": [ + { + "permission_level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + } +} { "method": "POST", "path": "/api/2.2/jobs/delete", diff --git a/acceptance/bundle/resources/permissions/jobs/update/out.requests_set_empty.direct.json b/acceptance/bundle/resources/permissions/jobs/update/out.requests_set_empty.direct.json index e69de29bb2..95d354f99a 100644 --- a/acceptance/bundle/resources/permissions/jobs/update/out.requests_set_empty.direct.json +++ b/acceptance/bundle/resources/permissions/jobs/update/out.requests_set_empty.direct.json @@ -0,0 +1,12 @@ +{ + "method": "PUT", + "path": "/api/2.0/permissions/jobs/[JOB_WITH_PERMISSIONS_ID]", + "body": { + "access_control_list": [ + { + "permission_level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + } +} diff --git a/acceptance/bundle/resources/permissions/output.txt b/acceptance/bundle/resources/permissions/output.txt index 85a16ad6a3..7090203247 100644 --- a/acceptance/bundle/resources/permissions/output.txt +++ b/acceptance/bundle/resources/permissions/output.txt @@ -180,7 +180,24 @@ DIFF jobs/delete_one/out.requests_destroy.direct.json "body": { "job_id": "[JOB_WITH_PERMISSIONS_ID]" EXACT jobs/empty_list/out.requests.deploy.direct.json -EXACT jobs/empty_list/out.requests.destroy.direct.json +DIFF jobs/empty_list/out.requests.destroy.direct.json +--- jobs/empty_list/out.requests.destroy.direct.json ++++ jobs/empty_list/out.requests.destroy.terraform.json +@@ -1 +1,14 @@ +-[]+[ ++ { ++ "body": { ++ "access_control_list": [ ++ { ++ "permission_level": "IS_OWNER", ++ "user_name": "[USERNAME]" ++ } ++ ] ++ }, ++ "method": "PUT", ++ "path": "/api/2.0/permissions/jobs/[NUMID]" ++ } ++] MATCH jobs/other_can_manage/out.requests.deploy.direct.json DIFF jobs/other_can_manage/out.requests.destroy.direct.json --- jobs/other_can_manage/out.requests.destroy.direct.json @@ -257,12 +274,11 @@ DIFF jobs/update/out.requests_delete_all.direct.json + "path": "/api/2.0/permissions/jobs/[JOB_WITH_PERMISSIONS_ID]" + } +] -EXACT jobs/update/out.requests_destroy.direct.json -DIFF jobs/update/out.requests_set_empty.direct.json ---- jobs/update/out.requests_set_empty.direct.json -+++ jobs/update/out.requests_set_empty.terraform.json -@@ -1 +1,14 @@ --[]+[ +DIFF jobs/update/out.requests_destroy.direct.json +--- jobs/update/out.requests_destroy.direct.json ++++ jobs/update/out.requests_destroy.terraform.json +@@ -1,4 +1,16 @@ + [ + { + "body": { + "access_control_list": [ @@ -274,8 +290,11 @@ DIFF jobs/update/out.requests_set_empty.direct.json + }, + "method": "PUT", + "path": "/api/2.0/permissions/jobs/[JOB_WITH_PERMISSIONS_ID]" -+ } -+] ++ }, + { + "body": { + "job_id": "[JOB_WITH_PERMISSIONS_ID]" +EXACT jobs/update/out.requests_set_empty.direct.json MATCH jobs/viewers/out.requests.deploy.direct.json DIFF jobs/viewers/out.requests.destroy.direct.json --- jobs/viewers/out.requests.destroy.direct.json diff --git a/acceptance/bundle/resources/permissions/pipelines/empty_list/databricks.yml b/acceptance/bundle/resources/permissions/pipelines/empty_list/databricks.yml index 6089d045ff..6db75dbfda 100644 --- a/acceptance/bundle/resources/permissions/pipelines/empty_list/databricks.yml +++ b/acceptance/bundle/resources/permissions/pipelines/empty_list/databricks.yml @@ -5,5 +5,5 @@ resources: pipelines: foo: name: foo - # this is interpreted the same as nil, but it's IMO a bug; it should do default IS_OWNER setting + # An explicit empty list is managed: FixPermissions adds the default IS_OWNER for the current user. permissions: [] diff --git a/acceptance/bundle/resources/permissions/pipelines/empty_list/out.plan.direct.json b/acceptance/bundle/resources/permissions/pipelines/empty_list/out.plan.direct.json index 9749323940..029250bd21 100644 --- a/acceptance/bundle/resources/permissions/pipelines/empty_list/out.plan.direct.json +++ b/acceptance/bundle/resources/permissions/pipelines/empty_list/out.plan.direct.json @@ -15,6 +15,29 @@ "name": "foo" } } + }, + "resources.pipelines.foo.permissions": { + "depends_on": [ + { + "node": "resources.pipelines.foo", + "label": "${resources.pipelines.foo.id}" + } + ], + "action": "create", + "new_state": { + "value": { + "object_id": "", + "__embed__": [ + { + "level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + }, + "vars": { + "object_id": "/pipelines/${resources.pipelines.foo.id}" + } + } } } } diff --git a/acceptance/bundle/resources/permissions/pipelines/empty_list/out.plan.terraform.json b/acceptance/bundle/resources/permissions/pipelines/empty_list/out.plan.terraform.json index 4305cb583e..58cb00a270 100644 --- a/acceptance/bundle/resources/permissions/pipelines/empty_list/out.plan.terraform.json +++ b/acceptance/bundle/resources/permissions/pipelines/empty_list/out.plan.terraform.json @@ -3,6 +3,9 @@ "plan": { "resources.pipelines.foo": { "action": "create" + }, + "resources.pipelines.foo.permissions": { + "action": "create" } } } diff --git a/acceptance/bundle/resources/permissions/pipelines/empty_list/out.requests.deploy.direct.json b/acceptance/bundle/resources/permissions/pipelines/empty_list/out.requests.deploy.direct.json index e69de29bb2..578163835c 100644 --- a/acceptance/bundle/resources/permissions/pipelines/empty_list/out.requests.deploy.direct.json +++ b/acceptance/bundle/resources/permissions/pipelines/empty_list/out.requests.deploy.direct.json @@ -0,0 +1,12 @@ +{ + "method": "PUT", + "path": "/api/2.0/permissions/pipelines/[UUID]", + "body": { + "access_control_list": [ + { + "permission_level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + } +} diff --git a/acceptance/bundle/resources/permissions/pipelines/empty_list/out.requests.deploy.terraform.json b/acceptance/bundle/resources/permissions/pipelines/empty_list/out.requests.deploy.terraform.json index e69de29bb2..578163835c 100644 --- a/acceptance/bundle/resources/permissions/pipelines/empty_list/out.requests.deploy.terraform.json +++ b/acceptance/bundle/resources/permissions/pipelines/empty_list/out.requests.deploy.terraform.json @@ -0,0 +1,12 @@ +{ + "method": "PUT", + "path": "/api/2.0/permissions/pipelines/[UUID]", + "body": { + "access_control_list": [ + { + "permission_level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + } +} diff --git a/acceptance/bundle/resources/permissions/pipelines/empty_list/output.txt b/acceptance/bundle/resources/permissions/pipelines/empty_list/output.txt index 2f3ce99457..89f7921475 100644 --- a/acceptance/bundle/resources/permissions/pipelines/empty_list/output.txt +++ b/acceptance/bundle/resources/permissions/pipelines/empty_list/output.txt @@ -1,6 +1,11 @@ >>> [CLI] bundle validate -o json -null +[ + { + "level": "IS_OWNER", + "user_name": "[USERNAME]" + } +] >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... diff --git a/bundle/config/mutator/resourcemutator/apply_bundle_permissions.go b/bundle/config/mutator/resourcemutator/apply_bundle_permissions.go index f2a6d85709..794035c801 100644 --- a/bundle/config/mutator/resourcemutator/apply_bundle_permissions.go +++ b/bundle/config/mutator/resourcemutator/apply_bundle_permissions.go @@ -122,6 +122,7 @@ func (m *bundlePermissions) Apply(ctx context.Context, b *bundle.Bundle) diag.Di v, err = dyn.MapByPattern(v, pattern, func(p dyn.Path, v dyn.Value) (dyn.Value, error) { var permissions []resources.Permission pv, err := dyn.Get(v, "permissions") + present := err == nil // If the permissions field is not found, we set to an empty array if err != nil { pv = dyn.V([]dyn.Value{}) @@ -140,7 +141,11 @@ func (m *bundlePermissions) Apply(ctx context.Context, b *bundle.Bundle) diag.Di levelsMap[key], )...) - if len(permissions) == 0 { + // Collapse to nil only when the block was absent, leaving the + // resource unmanaged. An explicit `permissions: []` must survive as + // an empty sequence so FixPermissions adds the default IS_OWNER, + // matching both a non-empty list and Terraform. + if len(permissions) == 0 && !present { permissions = nil } diff --git a/bundle/direct/bundle_plan.go b/bundle/direct/bundle_plan.go index 139012423e..a55af3955d 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -909,6 +909,20 @@ func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root return v, fmt.Errorf("unsupported resource type: %s", resourceType) } + // An empty grants list with no prior state has nothing to + // apply: creating the node would emit a no-op PATCH with no + // changes (terraform emits nothing). Skip it. When the node is + // already in state we must keep it so the removal is planned; + // the grant-removal PATCH is built from remote state, not config. + // This also covers permissions nodes, but an empty permissions + // list is populated with a default owner by FixPermissions + // before planning, so in practice only grants reach here empty. + if seq, ok := v.AsSequence(); ok && len(seq) == 0 { + if _, inState := db.State[s]; !inState { + return dyn.InvalidValue, nil + } + } + nodes = append(nodes, s) return dyn.InvalidValue, nil },