diff --git a/.nextchanges/bundles/empty-list-stays-enforced.md b/.nextchanges/bundles/empty-list-stays-enforced.md new file mode 100644 index 00000000000..5ae44436caf --- /dev/null +++ b/.nextchanges/bundles/empty-list-stays-enforced.md @@ -0,0 +1,2 @@ +Emptying a resource list now keeps enforcing it. A bundle declaring `grants: []` revokes +any grant added out of band on every deploy, not just the first. diff --git a/acceptance/bundle/resources/grants/schemas/empty_list_enforced/databricks.yml.tmpl b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/databricks.yml.tmpl new file mode 100644 index 00000000000..d157f693cd7 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/databricks.yml.tmpl @@ -0,0 +1,9 @@ +bundle: + name: schema-grants-oob-after-empty-$UNIQUE_NAME + +resources: + schemas: + grants_schema: + name: schema_oob_after_empty_$UNIQUE_NAME + catalog_name: main + grants: [{ principal: deco-test-user@databricks.com, privileges: [USE_SCHEMA] }] diff --git a/acceptance/bundle/resources/grants/schemas/empty_list_enforced/out.grants.direct.txt b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/out.grants.direct.txt new file mode 100644 index 00000000000..9534b2706f9 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/out.grants.direct.txt @@ -0,0 +1 @@ +json = {}; diff --git a/acceptance/bundle/resources/grants/schemas/empty_list_enforced/out.grants.terraform.txt b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/out.grants.terraform.txt new file mode 100644 index 00000000000..6a8c10fbd58 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/out.grants.terraform.txt @@ -0,0 +1,2 @@ +json.privilege_assignments[].principal = "deco-test-user@databricks.com"; +json.privilege_assignments[].privileges[] = "USE_SCHEMA"; diff --git a/acceptance/bundle/resources/grants/schemas/empty_list_enforced/out.test.toml b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/out.test.toml new file mode 100644 index 00000000000..cef45fe553a --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/out.test.toml @@ -0,0 +1,3 @@ +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/resources/grants/schemas/empty_list_enforced/output.txt b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/output.txt new file mode 100644 index 00000000000..1b21b4e0c1a --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/output.txt @@ -0,0 +1,30 @@ + +=== Deploy with one grant +>>> [CLI] bundle deploy -qq + +=== Empty the list: the grant is revoked +>>> [CLI] bundle deploy -qq + +>>> [CLI] grants get schema main.schema_oob_after_empty_[UNIQUE_NAME] +json = {}; + +=== Add a grant out of band +>>> [CLI] grants update schema main.schema_oob_after_empty_[UNIQUE_NAME] --json @update.json + +>>> [CLI] grants get schema main.schema_oob_after_empty_[UNIQUE_NAME] +json.privilege_assignments[].principal = "deco-test-user@databricks.com"; +json.privilege_assignments[].privileges[] = "USE_SCHEMA"; + +=== Redeploy: the enforced empty list revokes the out-of-band grant +>>> [CLI] bundle deploy -qq + +>>> 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-oob-after-empty-[UNIQUE_NAME]/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/resources/grants/schemas/empty_list_enforced/script b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/script new file mode 100644 index 00000000000..4f202b52de8 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/script @@ -0,0 +1,27 @@ +SCHEMA_FULL_NAME=main.schema_oob_after_empty_$UNIQUE_NAME + +envsubst < databricks.yml.tmpl > databricks.yml + +cleanup() { + trace errcode $CLI bundle destroy --auto-approve + rm -f out.requests.txt +} +trap cleanup EXIT + +title "Deploy with one grant" +trace $CLI bundle deploy -qq + +title "Empty the list: the grant is revoked" +update_file.py databricks.yml 'grants: [{ principal: deco-test-user@databricks.com, privileges: [USE_SCHEMA] }]' 'grants: []' +trace $CLI bundle deploy -qq +trace $CLI grants get schema "$SCHEMA_FULL_NAME" | gron.py --noindex | sort_lines.py --repl + +title "Add a grant out of band" +trace $CLI grants update schema "$SCHEMA_FULL_NAME" --json @update.json > /dev/null +trace $CLI grants get schema "$SCHEMA_FULL_NAME" | gron.py --noindex | sort_lines.py --repl | contains.py 'deco-test-user@databricks.com' + +# The engines diverge here, so the result goes to a per-engine file: direct re-plans the +# emptied node and revokes the out-of-band grant, terraform leaves it in place. +title "Redeploy: the enforced empty list revokes the out-of-band grant" +trace $CLI bundle deploy -qq +$CLI grants get schema "$SCHEMA_FULL_NAME" | gron.py --noindex | sort_lines.py --repl > out.grants.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/acceptance/bundle/resources/grants/schemas/empty_list_enforced/update.json b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/update.json new file mode 100644 index 00000000000..36945686643 --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/empty_list_enforced/update.json @@ -0,0 +1,8 @@ +{ + "changes": [ + { + "principal": "deco-test-user@databricks.com", + "add": ["USE_SCHEMA"] + } + ] +} diff --git a/acceptance/bundle/resources/grants/schemas/remove_all/out.plan.direct.txt b/acceptance/bundle/resources/grants/schemas/remove_all/out.plan.direct.txt new file mode 100644 index 00000000000..7860621b33f --- /dev/null +++ b/acceptance/bundle/resources/grants/schemas/remove_all/out.plan.direct.txt @@ -0,0 +1,11 @@ + +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/schema-grants-remove-all-[UNIQUE_NAME]/default/files... +Files: 3 uploaded, 0 deleted +Resources: 0 created, 0 changed, 0 deleted, 2 unchanged + +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged diff --git a/acceptance/bundle/resources/grants/schemas/remove_all/out.plan.txt b/acceptance/bundle/resources/grants/schemas/remove_all/out.plan.terraform.txt similarity index 100% rename from acceptance/bundle/resources/grants/schemas/remove_all/out.plan.txt rename to acceptance/bundle/resources/grants/schemas/remove_all/out.plan.terraform.txt diff --git a/acceptance/bundle/resources/grants/schemas/remove_all/script b/acceptance/bundle/resources/grants/schemas/remove_all/script index 6a16dafff30..12503be67c0 100644 --- a/acceptance/bundle/resources/grants/schemas/remove_all/script +++ b/acceptance/bundle/resources/grants/schemas/remove_all/script @@ -26,4 +26,4 @@ trace $CLI grants get schema main.schema_remove_all_$UNIQUE_NAME | gron.py --noi trace $CLI bundle plan trace $CLI bundle deploy trace $CLI bundle plan -} &> out.plan.txt +} &> out.plan.$DATABRICKS_BUNDLE_ENGINE.txt diff --git a/bundle/direct/apply.go b/bundle/direct/apply.go index 6b2fffe9a16..053f8f54951 100644 --- a/bundle/direct/apply.go +++ b/bundle/direct/apply.go @@ -159,24 +159,13 @@ func (d *DeploymentUnit) Update(ctx context.Context, db *dstate.DeploymentState, return err } - empty, err := d.Adapter.IsEmptyState(newState) + // An update that empties the resource out (e.g. all grants revoked) saves the empty state + // rather than dropping the entry. The resource is still there - the schema keeps existing, + // it just grants nothing - so the node stays tracked, and a caller reading the state sees + // what the update applied instead of nothing at all. + err = d.saveState(db, id, newState, d.DependsOn) if err != nil { - return err - } - - if empty { - // The update emptied the resource out (e.g. all grants revoked). Keeping an entry - // would report the node as tracked-and-unchanged forever, while a fresh deploy of - // the same config plans no node at all; drop it so the two agree. - err = db.DeleteState(d.ResourceKey) - if err != nil { - return fmt.Errorf("deleting state id=%s: %w", id, err) - } - } else { - err = d.saveState(db, id, newState, d.DependsOn) - if err != nil { - return fmt.Errorf("saving state id=%s: %w", id, err) - } + return fmt.Errorf("saving state id=%s: %w", id, err) } waitRemoteState, err := retryOnTransient(ctx, func() (any, error) {