Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d5c2ca3
Add declarative bind for direct deployment engine
shreyas-goenka May 7, 2026
9290944
Merge remote-tracking branch 'origin/main' into declarative-import-pr
shreyas-goenka Jul 13, 2026
20d1033
Address review: bind config-level duplicate-ID check + WaitAfterUpdate
shreyas-goenka Jul 13, 2026
944e8b9
Address review: closed-list bind validation, split Bind/BindAndUpdate…
shreyas-goenka Jul 13, 2026
618ca92
Fold bind planning into CalculatePlan
shreyas-goenka Jul 13, 2026
d30ed6e
lint: use tagged switch on action in bind dispatch
shreyas-goenka Jul 13, 2026
ad1d13c
Allow bundle destroy for resources with active bind blocks
janniklasrose Jul 22, 2026
cf77946
Preserve resize semantics for bind_and_update
janniklasrose Jul 22, 2026
205bcb3
Show full bind_and_update action in text plan
janniklasrose Jul 22, 2026
ed089ea
Keep bind block guarding against recreate after first bind
janniklasrose Jul 22, 2026
a9e80a9
Merge origin/main into janniklasrose/declarative-bind
janniklasrose Nov 3, 2026
532eef9
Reject bind blocks for resources missing from config at validation time
janniklasrose Nov 3, 2026
a6406e8
Error instead of recreating a bound resource that was deleted remotely
janniklasrose Nov 3, 2026
3862030
Reject a root-level bind block instead of ignoring it
janniklasrose Nov 3, 2026
1ee6fe3
Resolve variable references in bind IDs
janniklasrose Nov 3, 2026
525482c
Tighten bind planning guards and use the bind ID known at plan time
janniklasrose Nov 3, 2026
c120ed9
Warn on unbind when a bind block will re-bind the resource
janniklasrose Nov 3, 2026
54720c9
Drop the bind check from 'bundle deployment migrate'
janniklasrose Nov 3, 2026
c6d2ef1
Add acceptance coverage for bind scenarios from the design review
janniklasrose Nov 3, 2026
d4548f5
Document bind semantics and caveats in the schema and changelog
janniklasrose Nov 3, 2026
dd4e9c1
Add default branches to the bind action switches for the exhaustive l…
janniklasrose Nov 3, 2026
60e4eb1
Move bind under the resource lifecycle block
janniklasrose Nov 3, 2026
ab292e8
Link the new pull request from the bind changelog fragment
janniklasrose Nov 3, 2026
89c7ce5
Fix CI for lifecycle.bind: generated models, input subset test, cloud…
janniklasrose Nov 3, 2026
d202ea6
Regenerate the reference schema outputs for lifecycle.bind
janniklasrose Nov 3, 2026
0e9256f
Avoid leading-slash arguments in the in-state recreate guard test
janniklasrose Nov 3, 2026
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 .nextchanges/bundles/4630.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Add `lifecycle.bind` to resources to bring an existing workspace resource under bundle management at deploy time instead of creating a new one. The first deploy adopts the resource (`bind`) and applies the configuration if it differs (`bind_and_update`); while the block is present, deploys cannot recreate the resource, and `bundle destroy` still deletes it. ([#4630](https://github.com/databricks/cli/pull/4630), [#7321](https://github.com/databricks/cli/pull/7321))
19 changes: 19 additions & 0 deletions acceptance/bundle/deploy/bind/basic/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
bundle:
name: test-bind-basic-UNIQUE_NAME

resources:
jobs:
foo:
name: test-bind-job
environments:
- environment_key: default
spec:
client: "1"
tasks:
- task_key: my_task
environment_key: default
spark_python_task:
python_file: ./hello.py
lifecycle:
bind:
id: "PLACEHOLDER_JOB_ID"
1 change: 1 addition & 0 deletions acceptance/bundle/deploy/bind/basic/hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("hello")
2 changes: 2 additions & 0 deletions acceptance/bundle/deploy/bind/basic/out.test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Cloud = true
EnvMatrix.DMS = ["", "true"]
65 changes: 65 additions & 0 deletions acceptance/bundle/deploy/bind/basic/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@

>>> [CLI] bundle plan
bind_and_update jobs.foo (id: [NEW_JOB_ID])

Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged, 1 to bind

>>> [CLI] bundle deploy --auto-approve
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-basic-[UNIQUE_NAME]/default/files...
Bound and updated jobs.foo
Files: 2 uploaded, 0 deleted
Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound

>>> [CLI] bundle plan
Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged

>>> print_state.py
{
"state_version": 3,
"cli_version": "[CLI_VERSION]",
"lineage": "[UUID]",
"serial": 1,
"state": {
"resources.jobs.foo": {
"__id__": "[NEW_JOB_ID]",
"state": {
"deployment": {
"kind": "BUNDLE",
"metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bind-basic-[UNIQUE_NAME]/default/state/metadata.json"
},
"edit_mode": "UI_LOCKED",
"environments": [
{
"environment_key": "default",
"spec": {
"client": "1"
}
}
],
"format": "MULTI_TASK",
"max_concurrent_runs": 1,
"name": "test-bind-job",
"queue": {
"enabled": true
},
"tasks": [
{
"environment_key": "default",
"spark_python_task": {
"python_file": "/Workspace/Users/[USERNAME]/.bundle/test-bind-basic-[UNIQUE_NAME]/default/files/hello.py"
},
"task_key": "my_task"
}
]
}
}
}
}

>>> [CLI] bundle destroy --auto-approve
The following resources will be deleted:
delete resources.jobs.foo

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

Destroy: 1 deleted
25 changes: 25 additions & 0 deletions acceptance/bundle/deploy/bind/basic/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Bundle names carry $UNIQUE_NAME so concurrent runs on the same workspace do not share
# a root path and its deployment lock.
update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME"

# Create a job in the workspace
NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-import-job", "environments": [{"environment_key": "default", "spec": {"client": "1"}}], "tasks": [{"task_key": "my_task", "environment_key": "default", "spark_python_task": {"python_file": "/Workspace/test.py"}}]}' | jq -r .job_id)
add_repl.py $NEW_JOB_ID NEW_JOB_ID

# Update the databricks.yml with the actual job ID
update_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID"

# Run plan - should show import action
trace $CLI bundle plan

# Deploy with auto-approve
trace $CLI bundle deploy --auto-approve

# Plan again - should show no changes (skip)
trace $CLI bundle plan

# Verify state file contains the imported ID
trace print_state.py | contains.py "$NEW_JOB_ID"

# Cleanup: destroy is allowed even with an active bind block
trace $CLI bundle destroy --auto-approve
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
bundle:
name: test-bind-resize

workspace:
root_path: ~/.bundle/$UNIQUE_NAME

resources:
clusters:
test_cluster:
cluster_name: test-cluster-$UNIQUE_NAME
spark_version: $DEFAULT_SPARK_VERSION
instance_pool_id: $TEST_INSTANCE_POOL_ID
num_workers: 3
lifecycle:
bind:
id: "PLACEHOLDER_CLUSTER_ID"
2 changes: 2 additions & 0 deletions acceptance/bundle/deploy/bind/bind-and-resize/out.test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Cloud = true
EnvMatrix.DMS = ["", "true"]
37 changes: 37 additions & 0 deletions acceptance/bundle/deploy/bind/bind-and-resize/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

>>> [CLI] bundle plan
bind_and_update clusters.test_cluster (id: [CLUSTER_ID])

Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged, 1 to bind

>>> [CLI] bundle deploy --auto-approve
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/[UNIQUE_NAME]/files...
Bound and updated clusters.test_cluster
Files: 2 uploaded, 0 deleted
Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound

>>> print_requests.py //clusters/resize //clusters/edit
{
"method": "POST",
"path": "/api/2.1/clusters/resize",
"body": {
"cluster_id": "[CLUSTER_ID]",
"num_workers": 3
}
}

=== Cluster should have new num_workers

>>> [CLI] clusters get [CLUSTER_ID]
{
"cluster_name": "test-cluster-[UNIQUE_NAME]",
"num_workers": 3
}

>>> [CLI] bundle destroy --auto-approve
The following resources will be deleted:
delete resources.clusters.test_cluster

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

Destroy: 1 deleted
26 changes: 26 additions & 0 deletions acceptance/bundle/deploy/bind/bind-and-resize/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
envsubst < databricks.yml.tmpl > databricks.yml

# Create a cluster out-of-band, then bring it to RUNNING so that a num_workers
# change is planned as a resize rather than an edit. The cluster uses an instance
# pool only: the API rejects node_type_id together with instance_pool_id.
CLUSTER_ID=$($CLI clusters create --json "{\"cluster_name\": \"test-cluster-$UNIQUE_NAME\", \"spark_version\": \"$DEFAULT_SPARK_VERSION\", \"instance_pool_id\": \"$TEST_INSTANCE_POOL_ID\", \"num_workers\": 2, \"autotermination_minutes\": 60}" | jq -r .cluster_id)
add_repl.py "$CLUSTER_ID" CLUSTER_ID
update_file.py databricks.yml 'PLACEHOLDER_CLUSTER_ID' "$CLUSTER_ID"

# Transition PENDING -> RUNNING (ClustersGet promotes the state).
$CLI clusters get "$CLUSTER_ID" > /dev/null

# Plan: config differs only in num_workers, so the bound running cluster is
# planned as bind_and_update backed by a resize.
trace $CLI bundle plan

# Deploy: bind_and_update must go through the resize API, not clusters/edit.
trace $CLI bundle deploy --auto-approve
trace print_requests.py //clusters/resize //clusters/edit

title "Cluster should have new num_workers\n"
trace $CLI clusters get "$CLUSTER_ID" | jq '{cluster_name,num_workers}'

# Cleanup
trace $CLI bundle destroy --auto-approve
rm -f out.requests.txt
3 changes: 3 additions & 0 deletions acceptance/bundle/deploy/bind/bind-and-resize/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
RecordRequests = true

Ignore = [".databricks", "databricks.yml"]
19 changes: 19 additions & 0 deletions acceptance/bundle/deploy/bind/bind-and-update/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
bundle:
name: test-bind-update-UNIQUE_NAME

resources:
jobs:
foo:
name: updated-job-name
environments:
- environment_key: default
spec:
client: "1"
tasks:
- task_key: my_task
environment_key: default
spark_python_task:
python_file: ./hello.py
lifecycle:
bind:
id: "PLACEHOLDER_JOB_ID"
1 change: 1 addition & 0 deletions acceptance/bundle/deploy/bind/bind-and-update/hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("hello")
2 changes: 2 additions & 0 deletions acceptance/bundle/deploy/bind/bind-and-update/out.test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Cloud = true
EnvMatrix.DMS = ["", "true"]
36 changes: 36 additions & 0 deletions acceptance/bundle/deploy/bind/bind-and-update/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

>>> [CLI] bundle plan
bind_and_update jobs.foo (id: [NEW_JOB_ID])

Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged, 1 to bind

>>> [CLI] bundle deploy --auto-approve
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-update-[UNIQUE_NAME]/default/files...
Bound and updated jobs.foo
Files: 2 uploaded, 0 deleted
Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound

>>> [CLI] jobs get [NEW_JOB_ID]
updated-job-name

>>> [CLI] bundle plan
update jobs.foo

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

>>> [CLI] bundle deploy --auto-approve
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-update-[UNIQUE_NAME]/default/files...
Updated jobs.foo
Files: 1 uploaded, 0 deleted
Resources: 0 created, 1 changed, 0 deleted, 0 unchanged

>>> [CLI] jobs get [NEW_JOB_ID]
second-update-name

>>> [CLI] bundle destroy --auto-approve
The following resources will be deleted:
delete resources.jobs.foo

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

Destroy: 1 deleted
30 changes: 30 additions & 0 deletions acceptance/bundle/deploy/bind/bind-and-update/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME"

# Create a job in the workspace with a different name
NEW_JOB_ID=$($CLI jobs create --json '{"name": "original-job-name", "environments": [{"environment_key": "default", "spec": {"client": "1"}}], "tasks": [{"task_key": "my_task", "environment_key": "default", "spark_python_task": {"python_file": "/Workspace/test.py"}}]}' | jq -r .job_id)
add_repl.py $NEW_JOB_ID NEW_JOB_ID

# Update the databricks.yml with the actual job ID
update_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID"

# Run plan - should show bind action (name differs from config)
trace $CLI bundle plan

# Deploy with auto-approve
trace $CLI bundle deploy --auto-approve

# Verify the job was updated
trace $CLI jobs get $NEW_JOB_ID | jq -r .settings.name

# Now update the job name again in the config and deploy again.
# This time the action should be "update", not "bind", since the resource
# is already bound in state.
update_file.py databricks.yml 'updated-job-name' 'second-update-name'
trace $CLI bundle plan
trace $CLI bundle deploy --auto-approve

# Verify the job was updated with the second name
trace $CLI jobs get $NEW_JOB_ID | jq -r .settings.name

# Cleanup: destroy is allowed even with an active bind block
trace $CLI bundle destroy --auto-approve
14 changes: 14 additions & 0 deletions acceptance/bundle/deploy/bind/block-removed/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
bundle:
name: test-bind-block-removed-UNIQUE_NAME

resources:
pipelines:
foo:
name: test-pipeline
storage: /old/storage/path
libraries:
- notebook:
path: ./nb.sql
lifecycle:
bind:
id: "PLACEHOLDER_PIPELINE_ID"
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
bundle:
name: test-bind-block-removed-UNIQUE_NAME

resources:
pipelines:
foo:
name: test-pipeline
storage: /new/storage/path
libraries:
- notebook:
path: ./nb.sql
2 changes: 2 additions & 0 deletions acceptance/bundle/deploy/bind/block-removed/nb.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Databricks notebook source
select 1
2 changes: 2 additions & 0 deletions acceptance/bundle/deploy/bind/block-removed/out.test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Cloud = true
EnvMatrix.DMS = ["", "true"]
24 changes: 24 additions & 0 deletions acceptance/bundle/deploy/bind/block-removed/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

>>> [CLI] bundle deploy --auto-approve
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-block-removed-[UNIQUE_NAME]/default/files...
Bound and updated pipelines.foo
Files: 3 uploaded, 0 deleted
Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound

=== Removing the bind block is the opt-out: with it gone, the same storage change plans a recreate
>>> [CLI] bundle plan
recreate pipelines.foo

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

>>> [CLI] bundle destroy --auto-approve
The following resources will be deleted:
delete resources.pipelines.foo

This action will result in the deletion of the following Lakeflow Spark Declarative Pipelines along with the
Streaming Tables (STs) and Materialized Views (MVs) managed by them. Set 'cascade_on_destroy: false' on a pipeline to retain datasets on pipeline deletion:
delete resources.pipelines.foo

All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-block-removed-[UNIQUE_NAME]/default

Destroy: 1 deleted
16 changes: 16 additions & 0 deletions acceptance/bundle/deploy/bind/block-removed/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME"
update_file.py databricks_unbound.yml 'UNIQUE_NAME' "$UNIQUE_NAME"

# Create a pipeline whose storage matches the config and bind it
NEW_PIPELINE_ID=$($CLI pipelines create --json '{"name": "test-pipeline", "storage": "/old/storage/path", "allow_duplicate_names": true, "libraries": [{"notebook": {"path": "/Workspace/test"}}]}' | jq -r .pipeline_id)
add_repl.py $NEW_PIPELINE_ID NEW_PIPELINE_ID
update_file.py databricks.yml 'PLACEHOLDER_PIPELINE_ID' "$NEW_PIPELINE_ID"

trace $CLI bundle deploy --auto-approve

title "Removing the bind block is the opt-out: with it gone, the same storage change plans a recreate"
cp databricks_unbound.yml databricks.yml
trace $CLI bundle plan

# Cleanup
trace $CLI bundle destroy --auto-approve
1 change: 1 addition & 0 deletions acceptance/bundle/deploy/bind/block-removed/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ignore = [".databricks", "databricks_unbound.yml"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
bundle:
name: test-bind-delete-conflict-UNIQUE_NAME

resources:
jobs:
foo:
name: test-bind-delete-job
environments:
- environment_key: default
spec:
client: "1"
tasks:
- task_key: my_task
environment_key: default
spark_python_task:
python_file: ./hello.py
Loading
Loading