Skip to content

Commit 3a01ff0

Browse files
bugfix: Auto-include grants and permissions on --select (#5852)
## Changes Add `.grants` and `.permissions` of resources to plan ## Why Fixes #5794 `.grants` and `.permissions` live under their parents in the bundle, and most users will assume they are deployed as part of the `--select`ed resource. Alternative: make them addressable via `--select <type>.<name>.{grants,permissions}`. They already live as separate entities in plan, but this would be a separate improvement (if users _only_ want to deploy those). ## Tests New acceptance tests
1 parent 9a92921 commit 3a01ff0

8 files changed

Lines changed: 206 additions & 10 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* direct: Fix deploy bug when a `postgres_projects`, `postgres_branches`, or `postgres_endpoints` field is set to its zero value (e.g. `enable_pg_native_login: false`, `replace_existing: false`) ([#5782](https://github.com/databricks/cli/pull/5782)).
1818
* `bundle run --only` help now documents the `+` modifier syntax: prefix a task key with `+` to also run its upstream tasks, or suffix it with `+` for downstream tasks ([#5760](https://github.com/databricks/cli/pull/5760)).
1919
* direct: Recognize UC-managed catalog and schema property defaults to avoid unnecessary drift ([#5865](https://github.com/databricks/cli/pull/5865) & [#5870](https://github.com/databricks/cli/pull/5870)).
20+
* Fix `bundle deploy --select <resource>` skipping the resource's grants and permissions; they are now applied as part of the selected resource ([#5852](https://github.com/databricks/cli/pull/5852)).
2021

2122
### Dependency updates
2223

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
bundle:
2+
name: select-subnodes-$UNIQUE_NAME
3+
4+
resources:
5+
jobs:
6+
my_job:
7+
name: my_job-$UNIQUE_NAME
8+
permissions:
9+
- level: CAN_VIEW
10+
user_name: viewer@example.com
11+
12+
schemas:
13+
my_schema:
14+
catalog_name: main
15+
name: my_schema_$UNIQUE_NAME
16+
grants:
17+
- principal: deco-test-user@databricks.com
18+
privileges:
19+
- ALL_PRIVILEGES

acceptance/bundle/select/grants_permissions/out.test.toml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
2+
>>> [CLI] bundle plan --select jobs.my_job
3+
create jobs.my_job
4+
create jobs.my_job.permissions
5+
6+
Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged
7+
8+
>>> [CLI] bundle plan --select schemas.my_schema
9+
create schemas.my_schema
10+
create schemas.my_schema.grants
11+
12+
Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged
13+
14+
>>> [CLI] bundle plan --select jobs.my_job.permissions
15+
Error: no such resource: jobs.my_job.permissions
16+
17+
18+
>>> [CLI] bundle plan --select schemas.my_schema.grants
19+
Error: no such resource: schemas.my_schema.grants
20+
21+
22+
=== bundle deploy --select jobs.my_job
23+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/select-subnodes-[UNIQUE_NAME]/default/files...
24+
Deploying resources...
25+
Updating deployment state...
26+
Deployment complete!
27+
28+
=== bundle deploy --select schemas.my_schema
29+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/select-subnodes-[UNIQUE_NAME]/default/files...
30+
Deploying resources...
31+
Updating deployment state...
32+
Deployment complete!
33+
34+
>>> print_requests.py --sort //permissions
35+
{
36+
"method": "PATCH",
37+
"path": "/api/2.1/unity-catalog/permissions/schema/main.my_schema_[UNIQUE_NAME]",
38+
"body": {
39+
"changes": [
40+
{
41+
"add": [
42+
"ALL_PRIVILEGES"
43+
],
44+
"principal": "deco-test-user@databricks.com"
45+
}
46+
]
47+
}
48+
}
49+
{
50+
"method": "PUT",
51+
"path": "/api/2.0/permissions/jobs/[NUMID]",
52+
"body": {
53+
"access_control_list": [
54+
{
55+
"permission_level": "CAN_VIEW",
56+
"user_name": "viewer@example.com"
57+
},
58+
{
59+
"permission_level": "IS_OWNER",
60+
"user_name": "[USERNAME]"
61+
}
62+
]
63+
}
64+
}
65+
66+
=== Destroy
67+
>>> [CLI] bundle destroy --auto-approve
68+
The following resources will be deleted:
69+
delete resources.jobs.my_job
70+
delete resources.schemas.my_schema
71+
72+
This action will result in the deletion of the following UC schemas. Any underlying data may be lost:
73+
delete resources.schemas.my_schema
74+
75+
All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/select-subnodes-[UNIQUE_NAME]/default
76+
77+
Deleting files...
78+
Destroy complete!
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
envsubst '$UNIQUE_NAME' < databricks.yml.tmpl > databricks.yml
2+
3+
cleanup() {
4+
title "Destroy"
5+
trace $CLI bundle destroy --auto-approve
6+
rm -f out.requests.txt
7+
}
8+
trap cleanup EXIT
9+
10+
# Selecting a resource pulls in its grants/permissions sub-nodes even though the
11+
# dependency edge runs sub-node -> parent, so the plan would never reach them from
12+
# the parent otherwise (issue #5794).
13+
trace $CLI bundle plan --select jobs.my_job
14+
trace $CLI bundle plan --select schemas.my_schema
15+
16+
# Grants and permissions sub-nodes are not addressable via --select.
17+
musterr trace $CLI bundle plan --select jobs.my_job.permissions
18+
musterr trace $CLI bundle plan --select schemas.my_schema.grants
19+
20+
# Deploying a selected resource applies its permissions/grants too.
21+
$CLI bundle plan --select jobs.my_job -o json > plan-job.json
22+
title "bundle deploy --select jobs.my_job\n"
23+
$CLI bundle deploy --select jobs.my_job $(readplanarg plan-job.json)
24+
25+
$CLI bundle plan --select schemas.my_schema -o json > plan-schema.json
26+
title "bundle deploy --select schemas.my_schema\n"
27+
$CLI bundle deploy --select schemas.my_schema $(readplanarg plan-schema.json)
28+
29+
# Assert the permissions PUT and grants PATCH were emitted for the selected resources.
30+
trace print_requests.py --sort //permissions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Local-only: exercises --select (issue #5794) end to end against the testserver.
2+
# It deploys a schema, so it needs a UC metastore; --select is direct-engine only.
3+
# The bundle name deliberately avoids the "permissions" token used in the
4+
# print_requests.py filter: on Windows, Git Bash mangles the leading "//" of
5+
# "//permissions" so the filter degrades to the bare substring "permissions", which
6+
# would otherwise also match the bundle's own file-upload paths. (We can't set
7+
# MSYS_NO_PATHCONV to keep the "//" intact, because that breaks the python helper's
8+
# own path resolution -- see the env -u workaround in acceptance/script.prepare.)
9+
Local = true
10+
Cloud = false
11+
RecordRequests = true
12+
RequiresUnityCatalog = true
13+
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]
14+
EnvMatrix.READPLAN = ["", "1"]
15+
Ignore = [".databricks", ".gitignore", "databricks.yml", "plan-job.json", "plan-schema.json"]

bundle/deployplan/plan.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -216,23 +216,22 @@ func (p *Plan) FilterToSelected(selected []string) {
216216
reachable := make(map[string]struct{}, len(selected))
217217
for _, s := range selected {
218218
key := "resources." + s
219-
if _, ok := p.Plan[key]; ok {
220-
reachable[key] = struct{}{}
221-
queue = append(queue, key)
222-
}
219+
p.enqueueReachable(reachable, &queue, key)
220+
// Grants and permissions are modeled as separate plan nodes for internal
221+
// reasons, but the user cannot address them via --select. Pull them in as
222+
// part of the parent resource so selecting a resource applies its grants
223+
// and permissions too. The dependency edge runs sub-node → parent, so the
224+
// BFS below would never reach them from the parent otherwise.
225+
p.enqueueReachable(reachable, &queue, key+".grants")
226+
p.enqueueReachable(reachable, &queue, key+".permissions")
223227
}
224228

225229
// BFS following DependsOn edges to include transitive dependencies.
226230
for len(queue) > 0 {
227231
key := queue[0]
228232
queue = queue[1:]
229233
for _, dep := range p.Plan[key].DependsOn {
230-
if _, seen := reachable[dep.Node]; !seen {
231-
if _, ok := p.Plan[dep.Node]; ok {
232-
reachable[dep.Node] = struct{}{}
233-
queue = append(queue, dep.Node)
234-
}
235-
}
234+
p.enqueueReachable(reachable, &queue, dep.Node)
236235
}
237236
}
238237

@@ -243,6 +242,18 @@ func (p *Plan) FilterToSelected(selected []string) {
243242
}
244243
}
245244

245+
// enqueueReachable marks key as reachable and appends it to queue, if key exists
246+
// in the plan and has not been seen before. Missing or already-seen keys are ignored.
247+
func (p *Plan) enqueueReachable(reachable map[string]struct{}, queue *[]string, key string) {
248+
if _, seen := reachable[key]; seen {
249+
return
250+
}
251+
if _, ok := p.Plan[key]; ok {
252+
reachable[key] = struct{}{}
253+
*queue = append(*queue, key)
254+
}
255+
}
256+
246257
type lockmap struct {
247258
state map[string]int
248259
}

bundle/deployplan/plan_filter_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ func planWithDeps() *deployplan.Plan {
2020
return p
2121
}
2222

23+
func planWithGrants() *deployplan.Plan {
24+
p := deployplan.NewPlanDirect()
25+
p.Plan["resources.schemas.bronze"] = &deployplan.PlanEntry{}
26+
// Sub-nodes depend on the parent (to resolve full_name), not the other way around.
27+
p.Plan["resources.schemas.bronze.grants"] = &deployplan.PlanEntry{
28+
DependsOn: []deployplan.DependsOnEntry{{Node: "resources.schemas.bronze"}},
29+
}
30+
p.Plan["resources.schemas.bronze.permissions"] = &deployplan.PlanEntry{
31+
DependsOn: []deployplan.DependsOnEntry{{Node: "resources.schemas.bronze"}},
32+
}
33+
p.Plan["resources.schemas.silver"] = &deployplan.PlanEntry{}
34+
p.Plan["resources.schemas.silver.grants"] = &deployplan.PlanEntry{
35+
DependsOn: []deployplan.DependsOnEntry{{Node: "resources.schemas.silver"}},
36+
}
37+
return p
38+
}
39+
2340
func TestFilterToSelected_Direct(t *testing.T) {
2441
p := planWithDeps()
2542
p.FilterToSelected([]string{"jobs.foo"})
@@ -46,3 +63,23 @@ func TestFilterToSelected_Multiple(t *testing.T) {
4663
assert.NotContains(t, p.Plan, "resources.jobs.foo")
4764
assert.NotContains(t, p.Plan, "resources.jobs.bar")
4865
}
66+
67+
func TestFilterToSelected_IncludesGrantsAndPermissions(t *testing.T) {
68+
p := planWithGrants()
69+
p.FilterToSelected([]string{"schemas.bronze"})
70+
assert.Contains(t, p.Plan, "resources.schemas.bronze")
71+
assert.Contains(t, p.Plan, "resources.schemas.bronze.grants")
72+
assert.Contains(t, p.Plan, "resources.schemas.bronze.permissions")
73+
assert.NotContains(t, p.Plan, "resources.schemas.silver")
74+
assert.NotContains(t, p.Plan, "resources.schemas.silver.grants")
75+
}
76+
77+
func TestFilterToSelected_MissingSubNodesAreSkipped(t *testing.T) {
78+
p := planWithGrants()
79+
// silver has grants but no permissions node; selecting it must not fail.
80+
p.FilterToSelected([]string{"schemas.silver"})
81+
assert.Contains(t, p.Plan, "resources.schemas.silver")
82+
assert.Contains(t, p.Plan, "resources.schemas.silver.grants")
83+
assert.NotContains(t, p.Plan, "resources.schemas.silver.permissions")
84+
assert.NotContains(t, p.Plan, "resources.schemas.bronze")
85+
}

0 commit comments

Comments
 (0)