Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* 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)).
* `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)).
* 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)).
* 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)).

### Dependency updates

Expand Down
19 changes: 19 additions & 0 deletions acceptance/bundle/select/grants_permissions/databricks.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
bundle:
name: select-subnodes-$UNIQUE_NAME

resources:
jobs:
my_job:
name: my_job-$UNIQUE_NAME
permissions:
- level: CAN_VIEW
user_name: viewer@example.com

schemas:
my_schema:
catalog_name: main
name: my_schema_$UNIQUE_NAME
grants:
- principal: deco-test-user@databricks.com
privileges:
- ALL_PRIVILEGES
5 changes: 5 additions & 0 deletions acceptance/bundle/select/grants_permissions/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 78 additions & 0 deletions acceptance/bundle/select/grants_permissions/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@

>>> [CLI] bundle plan --select jobs.my_job
create jobs.my_job
create jobs.my_job.permissions

Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged

>>> [CLI] bundle plan --select schemas.my_schema
create schemas.my_schema
create schemas.my_schema.grants

Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged

>>> [CLI] bundle plan --select jobs.my_job.permissions
Error: no such resource: jobs.my_job.permissions


>>> [CLI] bundle plan --select schemas.my_schema.grants
Error: no such resource: schemas.my_schema.grants


=== bundle deploy --select jobs.my_job
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/select-subnodes-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

=== bundle deploy --select schemas.my_schema
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/select-subnodes-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> print_requests.py --sort //permissions
{
"method": "PATCH",
"path": "/api/2.1/unity-catalog/permissions/schema/main.my_schema_[UNIQUE_NAME]",
"body": {
"changes": [
{
"add": [
"ALL_PRIVILEGES"
],
"principal": "deco-test-user@databricks.com"
}
]
}
}
{
"method": "PUT",
"path": "/api/2.0/permissions/jobs/[NUMID]",
"body": {
"access_control_list": [
{
"permission_level": "CAN_VIEW",
"user_name": "viewer@example.com"
},
{
"permission_level": "IS_OWNER",
"user_name": "[USERNAME]"
}
]
}
}

=== Destroy
>>> [CLI] bundle destroy --auto-approve
The following resources will be deleted:
delete resources.jobs.my_job
delete resources.schemas.my_schema

This action will result in the deletion of the following UC schemas. Any underlying data may be lost:
delete resources.schemas.my_schema

All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/select-subnodes-[UNIQUE_NAME]/default

Deleting files...
Destroy complete!
30 changes: 30 additions & 0 deletions acceptance/bundle/select/grants_permissions/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
envsubst '$UNIQUE_NAME' < databricks.yml.tmpl > databricks.yml

cleanup() {
title "Destroy"
trace $CLI bundle destroy --auto-approve
rm -f out.requests.txt
}
trap cleanup EXIT

# Selecting a resource pulls in its grants/permissions sub-nodes even though the
# dependency edge runs sub-node -> parent, so the plan would never reach them from
# the parent otherwise (issue #5794).
trace $CLI bundle plan --select jobs.my_job
trace $CLI bundle plan --select schemas.my_schema

# Grants and permissions sub-nodes are not addressable via --select.
musterr trace $CLI bundle plan --select jobs.my_job.permissions
musterr trace $CLI bundle plan --select schemas.my_schema.grants

# Deploying a selected resource applies its permissions/grants too.
$CLI bundle plan --select jobs.my_job -o json > plan-job.json
title "bundle deploy --select jobs.my_job\n"
$CLI bundle deploy --select jobs.my_job $(readplanarg plan-job.json)

$CLI bundle plan --select schemas.my_schema -o json > plan-schema.json
title "bundle deploy --select schemas.my_schema\n"
$CLI bundle deploy --select schemas.my_schema $(readplanarg plan-schema.json)

# Assert the permissions PUT and grants PATCH were emitted for the selected resources.
trace print_requests.py --sort //permissions
15 changes: 15 additions & 0 deletions acceptance/bundle/select/grants_permissions/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Local-only: exercises --select (issue #5794) end to end against the testserver.
# It deploys a schema, so it needs a UC metastore; --select is direct-engine only.
# The bundle name deliberately avoids the "permissions" token used in the
# print_requests.py filter: on Windows, Git Bash mangles the leading "//" of
# "//permissions" so the filter degrades to the bare substring "permissions", which
# would otherwise also match the bundle's own file-upload paths. (We can't set
# MSYS_NO_PATHCONV to keep the "//" intact, because that breaks the python helper's
# own path resolution -- see the env -u workaround in acceptance/script.prepare.)
Local = true
Cloud = false
RecordRequests = true
RequiresUnityCatalog = true
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]
EnvMatrix.READPLAN = ["", "1"]
Ignore = [".databricks", ".gitignore", "databricks.yml", "plan-job.json", "plan-schema.json"]
31 changes: 21 additions & 10 deletions bundle/deployplan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,23 +216,22 @@ func (p *Plan) FilterToSelected(selected []string) {
reachable := make(map[string]struct{}, len(selected))
for _, s := range selected {
key := "resources." + s
if _, ok := p.Plan[key]; ok {
reachable[key] = struct{}{}
queue = append(queue, key)
}
p.enqueueReachable(reachable, &queue, key)
// Grants and permissions are modeled as separate plan nodes for internal
// reasons, but the user cannot address them via --select. Pull them in as
// part of the parent resource so selecting a resource applies its grants
// and permissions too. The dependency edge runs sub-node → parent, so the
// BFS below would never reach them from the parent otherwise.
p.enqueueReachable(reachable, &queue, key+".grants")
p.enqueueReachable(reachable, &queue, key+".permissions")
}

// BFS following DependsOn edges to include transitive dependencies.
for len(queue) > 0 {
key := queue[0]
queue = queue[1:]
for _, dep := range p.Plan[key].DependsOn {
if _, seen := reachable[dep.Node]; !seen {
if _, ok := p.Plan[dep.Node]; ok {
reachable[dep.Node] = struct{}{}
queue = append(queue, dep.Node)
}
}
p.enqueueReachable(reachable, &queue, dep.Node)
}
}

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

// enqueueReachable marks key as reachable and appends it to queue, if key exists
// in the plan and has not been seen before. Missing or already-seen keys are ignored.
func (p *Plan) enqueueReachable(reachable map[string]struct{}, queue *[]string, key string) {
if _, seen := reachable[key]; seen {
return
}
if _, ok := p.Plan[key]; ok {
reachable[key] = struct{}{}
*queue = append(*queue, key)
}
}

type lockmap struct {
state map[string]int
}
Expand Down
37 changes: 37 additions & 0 deletions bundle/deployplan/plan_filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ func planWithDeps() *deployplan.Plan {
return p
}

func planWithGrants() *deployplan.Plan {
p := deployplan.NewPlanDirect()
p.Plan["resources.schemas.bronze"] = &deployplan.PlanEntry{}
// Sub-nodes depend on the parent (to resolve full_name), not the other way around.
p.Plan["resources.schemas.bronze.grants"] = &deployplan.PlanEntry{
DependsOn: []deployplan.DependsOnEntry{{Node: "resources.schemas.bronze"}},
}
p.Plan["resources.schemas.bronze.permissions"] = &deployplan.PlanEntry{
DependsOn: []deployplan.DependsOnEntry{{Node: "resources.schemas.bronze"}},
}
p.Plan["resources.schemas.silver"] = &deployplan.PlanEntry{}
p.Plan["resources.schemas.silver.grants"] = &deployplan.PlanEntry{
DependsOn: []deployplan.DependsOnEntry{{Node: "resources.schemas.silver"}},
}
return p
}

func TestFilterToSelected_Direct(t *testing.T) {
p := planWithDeps()
p.FilterToSelected([]string{"jobs.foo"})
Expand All @@ -46,3 +63,23 @@ func TestFilterToSelected_Multiple(t *testing.T) {
assert.NotContains(t, p.Plan, "resources.jobs.foo")
assert.NotContains(t, p.Plan, "resources.jobs.bar")
}

func TestFilterToSelected_IncludesGrantsAndPermissions(t *testing.T) {
p := planWithGrants()
p.FilterToSelected([]string{"schemas.bronze"})
assert.Contains(t, p.Plan, "resources.schemas.bronze")
assert.Contains(t, p.Plan, "resources.schemas.bronze.grants")
assert.Contains(t, p.Plan, "resources.schemas.bronze.permissions")
assert.NotContains(t, p.Plan, "resources.schemas.silver")
assert.NotContains(t, p.Plan, "resources.schemas.silver.grants")
}

func TestFilterToSelected_MissingSubNodesAreSkipped(t *testing.T) {
p := planWithGrants()
// silver has grants but no permissions node; selecting it must not fail.
p.FilterToSelected([]string{"schemas.silver"})
assert.Contains(t, p.Plan, "resources.schemas.silver")
assert.Contains(t, p.Plan, "resources.schemas.silver.grants")
assert.NotContains(t, p.Plan, "resources.schemas.silver.permissions")
assert.NotContains(t, p.Plan, "resources.schemas.bronze")
}
Loading