From e90f116e3e8ae6dc43318e8cd1c8cb15fdb8f96b Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 12:59:33 +0000 Subject: [PATCH 01/20] Add declarative bind for the direct deployment engine Squash of the commits on PR #4630 (declarative-import), adapted to the current main: the terraform engine is gone, so bind runs on the only engine and the terraform gating is dropped; bind actions are wired into the per-resource deploy output, plan.CountActions and the deployment history operation types. Config shape at this point: targets..bind...id. The following commits move it under the resource lifecycle block. Co-authored-by: Shreyas Goenka Co-authored-by: Isaac --- .nextchanges/bundles/4630.md | 1 + .../bundle/deploy/bind/basic/databricks.yml | 23 ++ acceptance/bundle/deploy/bind/basic/hello.py | 1 + .../bundle/deploy/bind/basic/out.test.toml | 2 + .../bundle/deploy/bind/basic/output.txt | 65 +++++ acceptance/bundle/deploy/bind/basic/script | 21 ++ .../bind/bind-and-resize/databricks.yml.tmpl | 21 ++ .../deploy/bind/bind-and-resize/out.test.toml | 2 + .../deploy/bind/bind-and-resize/output.txt | 37 +++ .../bundle/deploy/bind/bind-and-resize/script | 25 ++ .../deploy/bind/bind-and-resize/test.toml | 3 + .../bind/bind-and-update/databricks.yml | 23 ++ .../deploy/bind/bind-and-update/hello.py | 1 + .../deploy/bind/bind-and-update/out.test.toml | 2 + .../deploy/bind/bind-and-update/output.txt | 36 +++ .../bundle/deploy/bind/bind-and-update/script | 28 ++ .../bind/bind-permissions/databricks.yml | 21 ++ .../bundle/deploy/bind/bind-permissions/nb.py | 2 + .../bind/bind-permissions/out.test.toml | 2 + .../deploy/bind/bind-permissions/output.txt | 13 + .../deploy/bind/bind-permissions/script | 3 + .../deploy/bind/block-migrate/databricks.yml | 23 ++ .../bundle/deploy/bind/block-migrate/hello.py | 1 + .../deploy/bind/block-migrate/out.test.toml | 2 + .../deploy/bind/block-migrate/output.txt | 3 + .../bundle/deploy/bind/block-migrate/script | 2 + .../deploy/bind/block-migrate/test.toml | 2 + .../delete-and-bind-conflict/databricks.yml | 19 ++ .../databricks_conflict.yml | 23 ++ .../bind/delete-and-bind-conflict/hello.py | 1 + .../delete-and-bind-conflict/out.test.toml | 2 + .../bind/delete-and-bind-conflict/output.txt | 21 ++ .../bind/delete-and-bind-conflict/script | 18 ++ .../bind/delete-and-bind-conflict/test.toml | 1 + .../bind/destroy-allowed/databricks.yml | 23 ++ .../deploy/bind/destroy-allowed/hello.py | 1 + .../deploy/bind/destroy-allowed/out.test.toml | 2 + .../deploy/bind/destroy-allowed/output.txt | 14 + .../bundle/deploy/bind/destroy-allowed/script | 13 + .../duplicate-bind-id-config/databricks.yml | 36 +++ .../bind/duplicate-bind-id-config/hello.py | 1 + .../duplicate-bind-id-config/out.test.toml | 2 + .../bind/duplicate-bind-id-config/output.txt | 10 + .../bind/duplicate-bind-id-config/script | 3 + .../bind/duplicate-bind-id/databricks.yml | 19 ++ .../databricks_with_bind.yml | 34 +++ .../deploy/bind/duplicate-bind-id/hello.py | 1 + .../bind/duplicate-bind-id/out.test.toml | 2 + .../deploy/bind/duplicate-bind-id/output.txt | 21 ++ .../deploy/bind/duplicate-bind-id/script | 18 ++ .../deploy/bind/duplicate-bind-id/test.toml | 1 + .../bind/invalid-resource-type/databricks.yml | 23 ++ .../bind/invalid-resource-type/hello.py | 1 + .../bind/invalid-resource-type/out.test.toml | 2 + .../bind/invalid-resource-type/output.txt | 6 + .../deploy/bind/invalid-resource-type/script | 1 + .../deploy/bind/orphaned-bind/databricks.yml | 23 ++ .../bundle/deploy/bind/orphaned-bind/hello.py | 1 + .../deploy/bind/orphaned-bind/out.test.toml | 2 + .../deploy/bind/orphaned-bind/output.txt | 7 + .../bundle/deploy/bind/orphaned-bind/script | 2 + .../recreate-blocked-in-state/databricks.yml | 18 ++ .../bind/recreate-blocked-in-state/nb.sql | 2 + .../recreate-blocked-in-state/out.test.toml | 2 + .../bind/recreate-blocked-in-state/output.txt | 38 +++ .../bind/recreate-blocked-in-state/script | 15 + .../bind/recreate-blocked/databricks.yml | 18 ++ .../deploy/bind/recreate-blocked/nb.sql | 2 + .../bind/recreate-blocked/out.test.toml | 2 + .../deploy/bind/recreate-blocked/output.txt | 32 ++ .../deploy/bind/recreate-blocked/script | 9 + .../bind/resource-not-found/databricks.yml | 23 ++ .../deploy/bind/resource-not-found/hello.py | 1 + .../bind/resource-not-found/out.test.toml | 2 + .../deploy/bind/resource-not-found/output.txt | 6 + .../deploy/bind/resource-not-found/script | 2 + acceptance/bundle/deploy/bind/test.toml | 2 + .../deploy/bind/top-level-bind/databricks.yml | 21 ++ .../deploy/bind/top-level-bind/hello.py | 1 + .../deploy/bind/top-level-bind/out.test.toml | 2 + .../deploy/bind/top-level-bind/output.txt | 12 + .../bundle/deploy/bind/top-level-bind/script | 3 + .../deploy/bind/top-level-bind/test.toml | 1 + acceptance/test.toml | 2 +- bundle/bundle.go | 8 + bundle/config/bind.go | 73 +++++ bundle/config/bind_test.go | 70 +++++ .../config/mutator/validate_bind_resources.go | 29 ++ .../mutator/validate_bind_resources_test.go | 40 +++ bundle/config/target.go | 5 + bundle/deployplan/action.go | 53 ++-- bundle/deployplan/plan.go | 9 + bundle/direct/apply.go | 43 ++- bundle/direct/bind.go | 4 +- bundle/direct/bundle_plan.go | 36 ++- bundle/direct/bundle_plan_bind.go | 276 ++++++++++++++++++ bundle/internal/schema/annotations.yml | 7 + bundle/phases/deploy.go | 5 +- bundle/phases/destroy.go | 7 +- bundle/phases/dms.go | 4 + bundle/phases/initialize.go | 4 + bundle/schema/jsonschema.json | 63 ++++ bundle/statemgmt/direct_migration.go | 3 +- cmd/bundle/config_remote_sync.go | 2 +- cmd/bundle/deployment/migrate.go | 8 + cmd/bundle/plan.go | 14 +- 106 files changed, 1669 insertions(+), 29 deletions(-) create mode 100644 .nextchanges/bundles/4630.md create mode 100644 acceptance/bundle/deploy/bind/basic/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/basic/hello.py create mode 100644 acceptance/bundle/deploy/bind/basic/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/basic/output.txt create mode 100644 acceptance/bundle/deploy/bind/basic/script create mode 100644 acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl create mode 100644 acceptance/bundle/deploy/bind/bind-and-resize/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/bind-and-resize/output.txt create mode 100644 acceptance/bundle/deploy/bind/bind-and-resize/script create mode 100644 acceptance/bundle/deploy/bind/bind-and-resize/test.toml create mode 100644 acceptance/bundle/deploy/bind/bind-and-update/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/bind-and-update/hello.py create mode 100644 acceptance/bundle/deploy/bind/bind-and-update/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/bind-and-update/output.txt create mode 100644 acceptance/bundle/deploy/bind/bind-and-update/script create mode 100644 acceptance/bundle/deploy/bind/bind-permissions/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/bind-permissions/nb.py create mode 100644 acceptance/bundle/deploy/bind/bind-permissions/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/bind-permissions/output.txt create mode 100755 acceptance/bundle/deploy/bind/bind-permissions/script create mode 100644 acceptance/bundle/deploy/bind/block-migrate/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/block-migrate/hello.py create mode 100644 acceptance/bundle/deploy/bind/block-migrate/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/block-migrate/output.txt create mode 100644 acceptance/bundle/deploy/bind/block-migrate/script create mode 100644 acceptance/bundle/deploy/bind/block-migrate/test.toml create mode 100644 acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks_conflict.yml create mode 100644 acceptance/bundle/deploy/bind/delete-and-bind-conflict/hello.py create mode 100644 acceptance/bundle/deploy/bind/delete-and-bind-conflict/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/delete-and-bind-conflict/output.txt create mode 100644 acceptance/bundle/deploy/bind/delete-and-bind-conflict/script create mode 100644 acceptance/bundle/deploy/bind/delete-and-bind-conflict/test.toml create mode 100644 acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/destroy-allowed/hello.py create mode 100644 acceptance/bundle/deploy/bind/destroy-allowed/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/destroy-allowed/output.txt create mode 100644 acceptance/bundle/deploy/bind/destroy-allowed/script create mode 100644 acceptance/bundle/deploy/bind/duplicate-bind-id-config/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/duplicate-bind-id-config/hello.py create mode 100644 acceptance/bundle/deploy/bind/duplicate-bind-id-config/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/duplicate-bind-id-config/output.txt create mode 100644 acceptance/bundle/deploy/bind/duplicate-bind-id-config/script create mode 100644 acceptance/bundle/deploy/bind/duplicate-bind-id/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/duplicate-bind-id/databricks_with_bind.yml create mode 100644 acceptance/bundle/deploy/bind/duplicate-bind-id/hello.py create mode 100644 acceptance/bundle/deploy/bind/duplicate-bind-id/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/duplicate-bind-id/output.txt create mode 100644 acceptance/bundle/deploy/bind/duplicate-bind-id/script create mode 100644 acceptance/bundle/deploy/bind/duplicate-bind-id/test.toml create mode 100644 acceptance/bundle/deploy/bind/invalid-resource-type/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/invalid-resource-type/hello.py create mode 100644 acceptance/bundle/deploy/bind/invalid-resource-type/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/invalid-resource-type/output.txt create mode 100644 acceptance/bundle/deploy/bind/invalid-resource-type/script create mode 100644 acceptance/bundle/deploy/bind/orphaned-bind/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/orphaned-bind/hello.py create mode 100644 acceptance/bundle/deploy/bind/orphaned-bind/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/orphaned-bind/output.txt create mode 100644 acceptance/bundle/deploy/bind/orphaned-bind/script create mode 100644 acceptance/bundle/deploy/bind/recreate-blocked-in-state/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/recreate-blocked-in-state/nb.sql create mode 100644 acceptance/bundle/deploy/bind/recreate-blocked-in-state/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt create mode 100644 acceptance/bundle/deploy/bind/recreate-blocked-in-state/script create mode 100644 acceptance/bundle/deploy/bind/recreate-blocked/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/recreate-blocked/nb.sql create mode 100644 acceptance/bundle/deploy/bind/recreate-blocked/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/recreate-blocked/output.txt create mode 100644 acceptance/bundle/deploy/bind/recreate-blocked/script create mode 100644 acceptance/bundle/deploy/bind/resource-not-found/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/resource-not-found/hello.py create mode 100644 acceptance/bundle/deploy/bind/resource-not-found/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/resource-not-found/output.txt create mode 100644 acceptance/bundle/deploy/bind/resource-not-found/script create mode 100644 acceptance/bundle/deploy/bind/test.toml create mode 100644 acceptance/bundle/deploy/bind/top-level-bind/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/top-level-bind/hello.py create mode 100644 acceptance/bundle/deploy/bind/top-level-bind/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/top-level-bind/output.txt create mode 100644 acceptance/bundle/deploy/bind/top-level-bind/script create mode 100644 acceptance/bundle/deploy/bind/top-level-bind/test.toml create mode 100644 bundle/config/bind.go create mode 100644 bundle/config/bind_test.go create mode 100644 bundle/config/mutator/validate_bind_resources.go create mode 100644 bundle/config/mutator/validate_bind_resources_test.go create mode 100644 bundle/direct/bundle_plan_bind.go diff --git a/.nextchanges/bundles/4630.md b/.nextchanges/bundles/4630.md new file mode 100644 index 00000000000..f0dd7f82b22 --- /dev/null +++ b/.nextchanges/bundles/4630.md @@ -0,0 +1 @@ +* engine/direct: Add declarative `bind` blocks under a target to bring existing workspace resources under bundle management at deploy time, with `bind` and `bind_and_update` actions surfaced in `bundle plan` output ([#4630](https://github.com/databricks/cli/pull/4630)). diff --git a/acceptance/bundle/deploy/bind/basic/databricks.yml b/acceptance/bundle/deploy/bind/basic/databricks.yml new file mode 100644 index 00000000000..a6b280560ab --- /dev/null +++ b/acceptance/bundle/deploy/bind/basic/databricks.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-basic + +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 + +targets: + default: + bind: + jobs: + foo: + id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/basic/hello.py b/acceptance/bundle/deploy/bind/basic/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/deploy/bind/basic/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/deploy/bind/basic/out.test.toml b/acceptance/bundle/deploy/bind/basic/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/basic/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/basic/output.txt b/acceptance/bundle/deploy/bind/basic/output.txt new file mode 100644 index 00000000000..c8ad40f18b0 --- /dev/null +++ b/acceptance/bundle/deploy/bind/basic/output.txt @@ -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/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/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/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/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/basic/script b/acceptance/bundle/deploy/bind/basic/script new file mode 100644 index 00000000000..fc56b7caad2 --- /dev/null +++ b/acceptance/bundle/deploy/bind/basic/script @@ -0,0 +1,21 @@ +# 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 diff --git a/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl b/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl new file mode 100644 index 00000000000..95b9638eafe --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl @@ -0,0 +1,21 @@ +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 + node_type_id: $NODE_TYPE_ID + instance_pool_id: $TEST_INSTANCE_POOL_ID + num_workers: 3 + +targets: + default: + bind: + clusters: + test_cluster: + id: "PLACEHOLDER_CLUSTER_ID" diff --git a/acceptance/bundle/deploy/bind/bind-and-resize/out.test.toml b/acceptance/bundle/deploy/bind/bind-and-resize/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-resize/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/bind-and-resize/output.txt b/acceptance/bundle/deploy/bind/bind-and-resize/output.txt new file mode 100644 index 00000000000..5e7ecc9f876 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-resize/output.txt @@ -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 diff --git a/acceptance/bundle/deploy/bind/bind-and-resize/script b/acceptance/bundle/deploy/bind/bind-and-resize/script new file mode 100644 index 00000000000..8b77a43b9db --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-resize/script @@ -0,0 +1,25 @@ +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. +CLUSTER_ID=$($CLI clusters create --json "{\"cluster_name\": \"test-cluster-$UNIQUE_NAME\", \"spark_version\": \"$DEFAULT_SPARK_VERSION\", \"node_type_id\": \"$NODE_TYPE_ID\", \"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 diff --git a/acceptance/bundle/deploy/bind/bind-and-resize/test.toml b/acceptance/bundle/deploy/bind/bind-and-resize/test.toml new file mode 100644 index 00000000000..e7593c38d40 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-resize/test.toml @@ -0,0 +1,3 @@ +RecordRequests = true + +Ignore = [".databricks", "databricks.yml"] diff --git a/acceptance/bundle/deploy/bind/bind-and-update/databricks.yml b/acceptance/bundle/deploy/bind/bind-and-update/databricks.yml new file mode 100644 index 00000000000..3b813772d02 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-update/databricks.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-update + +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 + +targets: + default: + bind: + jobs: + foo: + id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/bind-and-update/hello.py b/acceptance/bundle/deploy/bind/bind-and-update/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-update/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/deploy/bind/bind-and-update/out.test.toml b/acceptance/bundle/deploy/bind/bind-and-update/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-update/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/bind-and-update/output.txt b/acceptance/bundle/deploy/bind/bind-and-update/output.txt new file mode 100644 index 00000000000..87bf7c30d14 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-update/output.txt @@ -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/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/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/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/bind-and-update/script b/acceptance/bundle/deploy/bind/bind-and-update/script new file mode 100644 index 00000000000..1424e8b5b1c --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-update/script @@ -0,0 +1,28 @@ +# 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 diff --git a/acceptance/bundle/deploy/bind/bind-permissions/databricks.yml b/acceptance/bundle/deploy/bind/bind-permissions/databricks.yml new file mode 100644 index 00000000000..ff4bc4534d3 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-permissions/databricks.yml @@ -0,0 +1,21 @@ +bundle: + name: test-bind-permissions + +resources: + jobs: + foo: + name: test-job + tasks: + - task_key: test + notebook_task: + notebook_path: ./nb.py + permissions: + - group_name: users + level: CAN_MANAGE + +targets: + default: + bind: + jobs.permissions: + foo: + id: "12345" diff --git a/acceptance/bundle/deploy/bind/bind-permissions/nb.py b/acceptance/bundle/deploy/bind/bind-permissions/nb.py new file mode 100644 index 00000000000..38d86b79c70 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-permissions/nb.py @@ -0,0 +1,2 @@ +# Databricks notebook source +print("Hello, World!") diff --git a/acceptance/bundle/deploy/bind/bind-permissions/out.test.toml b/acceptance/bundle/deploy/bind/bind-permissions/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-permissions/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/bind-permissions/output.txt b/acceptance/bundle/deploy/bind/bind-permissions/output.txt new file mode 100644 index 00000000000..1150649418d --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-permissions/output.txt @@ -0,0 +1,13 @@ + +>>> musterr [CLI] bundle validate +Error: binding "jobs.permissions" is not supported + +"jobs.permissions" is not a supported resource type; bind is only supported for resources that can be defined under the resources block. + +Name: test-bind-permissions +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bind-permissions/default + +Found 1 error diff --git a/acceptance/bundle/deploy/bind/bind-permissions/script b/acceptance/bundle/deploy/bind/bind-permissions/script new file mode 100755 index 00000000000..a34e6208ad9 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-permissions/script @@ -0,0 +1,3 @@ +#!/bin/bash + +trace musterr $CLI bundle validate diff --git a/acceptance/bundle/deploy/bind/block-migrate/databricks.yml b/acceptance/bundle/deploy/bind/block-migrate/databricks.yml new file mode 100644 index 00000000000..16cc8b2be6f --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-migrate/databricks.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-block-migrate + +resources: + jobs: + foo: + name: test-job + environments: + - environment_key: default + spec: + client: "1" + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: ./hello.py + +targets: + default: + bind: + jobs: + foo: + id: "12345" diff --git a/acceptance/bundle/deploy/bind/block-migrate/hello.py b/acceptance/bundle/deploy/bind/block-migrate/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-migrate/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/deploy/bind/block-migrate/out.test.toml b/acceptance/bundle/deploy/bind/block-migrate/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-migrate/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/block-migrate/output.txt b/acceptance/bundle/deploy/bind/block-migrate/output.txt new file mode 100644 index 00000000000..6bc78e4ace6 --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-migrate/output.txt @@ -0,0 +1,3 @@ + +>>> musterr [CLI] bundle deployment migrate +Error: cannot run 'bundle deployment migrate' when bind blocks are defined in the target configuration; bind blocks are only supported with the direct deployment engine diff --git a/acceptance/bundle/deploy/bind/block-migrate/script b/acceptance/bundle/deploy/bind/block-migrate/script new file mode 100644 index 00000000000..64c538ec319 --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-migrate/script @@ -0,0 +1,2 @@ +# Try to run migration with bind blocks - should fail +trace musterr $CLI bundle deployment migrate diff --git a/acceptance/bundle/deploy/bind/block-migrate/test.toml b/acceptance/bundle/deploy/bind/block-migrate/test.toml new file mode 100644 index 00000000000..680c17c1e01 --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-migrate/test.toml @@ -0,0 +1,2 @@ +# Migration test does not need engine matrix +[EnvMatrix] diff --git a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks.yml b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks.yml new file mode 100644 index 00000000000..cbb7da54afe --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks.yml @@ -0,0 +1,19 @@ +bundle: + name: test-bind-delete-conflict + +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 + +targets: + default: diff --git a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks_conflict.yml b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks_conflict.yml new file mode 100644 index 00000000000..59758817069 --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks_conflict.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-delete-conflict + +resources: + jobs: + bar: + 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 + +targets: + default: + bind: + jobs: + bar: + id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/hello.py b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/out.test.toml b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/output.txt b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/output.txt new file mode 100644 index 00000000000..f6ca7b4088f --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/output.txt @@ -0,0 +1,21 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-delete-conflict/default/files... +Created jobs.foo +Files: 3 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +>>> musterr [CLI] bundle plan +Error: bind block for "resources.jobs.bar" has the same ID "[FOO_ID]" as existing resource "resources.jobs.foo"; remove the bind block or the conflicting resource + at targets.default.bind.jobs.bar + +Error: bind validation failed + + +>>> [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-delete-conflict/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/script b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/script new file mode 100644 index 00000000000..6547472f40f --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/script @@ -0,0 +1,18 @@ +# Deploy foo to create it in state +trace $CLI bundle deploy --auto-approve + +# Get the job ID from state +JOB_ID=$(read_id.py foo) + +# Switch to a config that renames foo->bar and adds a bind block for bar +# with the same job ID. This creates a conflict: foo is being deleted +# (still in state) while bar is being bound with the same ID. +cp databricks.yml databricks.yml.bak +cp databricks_conflict.yml databricks.yml +update_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$JOB_ID" + +trace musterr $CLI bundle plan + +# Cleanup: restore original config and destroy +cp databricks.yml.bak databricks.yml +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/test.toml b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/test.toml new file mode 100644 index 00000000000..a07a7675610 --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/test.toml @@ -0,0 +1 @@ +Ignore = [".databricks", "databricks.yml.bak", "databricks_conflict.yml"] diff --git a/acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml b/acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml new file mode 100644 index 00000000000..0a0cf4c7693 --- /dev/null +++ b/acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-destroy-allowed + +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 + +targets: + default: + bind: + jobs: + foo: + id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/destroy-allowed/hello.py b/acceptance/bundle/deploy/bind/destroy-allowed/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/deploy/bind/destroy-allowed/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/deploy/bind/destroy-allowed/out.test.toml b/acceptance/bundle/deploy/bind/destroy-allowed/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/destroy-allowed/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/destroy-allowed/output.txt b/acceptance/bundle/deploy/bind/destroy-allowed/output.txt new file mode 100644 index 00000000000..476823d966c --- /dev/null +++ b/acceptance/bundle/deploy/bind/destroy-allowed/output.txt @@ -0,0 +1,14 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-destroy-allowed/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 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-destroy-allowed/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/destroy-allowed/script b/acceptance/bundle/deploy/bind/destroy-allowed/script new file mode 100644 index 00000000000..75d366070f9 --- /dev/null +++ b/acceptance/bundle/deploy/bind/destroy-allowed/script @@ -0,0 +1,13 @@ +# Create a job in the workspace +NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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" + +# Deploy to bind the resource into state +trace $CLI bundle deploy --auto-approve + +# Destroy is allowed even with an active bind block: naming destroy explicitly +# captures the intent to tear the deployment down. +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id-config/databricks.yml b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/databricks.yml new file mode 100644 index 00000000000..528d389e53a --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/databricks.yml @@ -0,0 +1,36 @@ +bundle: + name: test-bind-duplicate-id-config + +resources: + jobs: + foo: + name: test-bind-dup-config-foo + environments: + - environment_key: default + spec: + client: "1" + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: ./hello.py + bar: + name: test-bind-dup-config-bar + environments: + - environment_key: default + spec: + client: "1" + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: ./hello.py + +targets: + default: + bind: + jobs: + foo: + id: "12345" + bar: + id: "12345" diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id-config/hello.py b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id-config/out.test.toml b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id-config/output.txt b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/output.txt new file mode 100644 index 00000000000..07b7ec4b6ed --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/output.txt @@ -0,0 +1,10 @@ + +>>> musterr [CLI] bundle plan +Error: bind block for "resources.jobs.bar" has the same ID "12345" as bind block(s) for resources.jobs.foo; each workspace resource can only be bound once + at targets.default.bind.jobs.bar + +Error: bind block for "resources.jobs.foo" has the same ID "12345" as bind block(s) for resources.jobs.bar; each workspace resource can only be bound once + at targets.default.bind.jobs.foo + +Error: bind validation failed + diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id-config/script b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/script new file mode 100644 index 00000000000..6c1284521fe --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/script @@ -0,0 +1,3 @@ +# Two first-time bind blocks pointing at the same workspace ID must be rejected; +# otherwise both would create state entries managing one remote resource. +trace musterr $CLI bundle plan diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks.yml b/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks.yml new file mode 100644 index 00000000000..9ae092f1907 --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks.yml @@ -0,0 +1,19 @@ +bundle: + name: test-bind-duplicate-id + +resources: + jobs: + foo: + name: test-bind-dup-job + environments: + - environment_key: default + spec: + client: "1" + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: ./hello.py + +targets: + default: diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks_with_bind.yml b/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks_with_bind.yml new file mode 100644 index 00000000000..0b3abd007f2 --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks_with_bind.yml @@ -0,0 +1,34 @@ +bundle: + name: test-bind-duplicate-id + +resources: + jobs: + foo: + name: test-bind-dup-job + environments: + - environment_key: default + spec: + client: "1" + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: ./hello.py + bar: + name: test-bind-dup-bar + environments: + - environment_key: default + spec: + client: "1" + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: ./hello.py + +targets: + default: + bind: + jobs: + bar: + id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id/hello.py b/acceptance/bundle/deploy/bind/duplicate-bind-id/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id/out.test.toml b/acceptance/bundle/deploy/bind/duplicate-bind-id/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id/output.txt b/acceptance/bundle/deploy/bind/duplicate-bind-id/output.txt new file mode 100644 index 00000000000..f573ef27a79 --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/output.txt @@ -0,0 +1,21 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-duplicate-id/default/files... +Created jobs.foo +Files: 3 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +>>> musterr [CLI] bundle plan +Error: bind block for "resources.jobs.bar" has the same ID "[FOO_ID]" as existing resource "resources.jobs.foo"; remove the bind block or the conflicting resource + at targets.default.bind.jobs.bar + +Error: bind validation failed + + +>>> [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-duplicate-id/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id/script b/acceptance/bundle/deploy/bind/duplicate-bind-id/script new file mode 100644 index 00000000000..57ab78fcf0f --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/script @@ -0,0 +1,18 @@ +# Deploy foo to create it in state +trace $CLI bundle deploy --auto-approve + +# Get foo's job ID from state +JOB_ID=$(read_id.py foo) + +# Switch to a config that keeps foo AND adds bar with a bind block +# pointing to foo's ID. This is a conflict: foo is still managed in +# state and bar tries to bind the same resource ID. +cp databricks.yml databricks.yml.bak +cp databricks_with_bind.yml databricks.yml +update_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$JOB_ID" + +trace musterr $CLI bundle plan + +# Cleanup +cp databricks.yml.bak databricks.yml +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id/test.toml b/acceptance/bundle/deploy/bind/duplicate-bind-id/test.toml new file mode 100644 index 00000000000..f70b5c78fff --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/test.toml @@ -0,0 +1 @@ +Ignore = [".databricks", "databricks.yml.bak", "databricks_with_bind.yml"] diff --git a/acceptance/bundle/deploy/bind/invalid-resource-type/databricks.yml b/acceptance/bundle/deploy/bind/invalid-resource-type/databricks.yml new file mode 100644 index 00000000000..84e59cd7f52 --- /dev/null +++ b/acceptance/bundle/deploy/bind/invalid-resource-type/databricks.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-invalid-resource-type + +resources: + jobs: + foo: + name: test-job + environments: + - environment_key: default + spec: + client: "1" + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: ./hello.py + +targets: + default: + bind: + foobar: + foo: + id: "123" diff --git a/acceptance/bundle/deploy/bind/invalid-resource-type/hello.py b/acceptance/bundle/deploy/bind/invalid-resource-type/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/deploy/bind/invalid-resource-type/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/deploy/bind/invalid-resource-type/out.test.toml b/acceptance/bundle/deploy/bind/invalid-resource-type/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/invalid-resource-type/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/invalid-resource-type/output.txt b/acceptance/bundle/deploy/bind/invalid-resource-type/output.txt new file mode 100644 index 00000000000..eb33b3f42ba --- /dev/null +++ b/acceptance/bundle/deploy/bind/invalid-resource-type/output.txt @@ -0,0 +1,6 @@ + +>>> musterr [CLI] bundle plan +Error: binding "foobar" is not supported + +"foobar" is not a supported resource type; bind is only supported for resources that can be defined under the resources block. + diff --git a/acceptance/bundle/deploy/bind/invalid-resource-type/script b/acceptance/bundle/deploy/bind/invalid-resource-type/script new file mode 100644 index 00000000000..9d9604578f1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/invalid-resource-type/script @@ -0,0 +1 @@ +trace musterr $CLI bundle plan diff --git a/acceptance/bundle/deploy/bind/orphaned-bind/databricks.yml b/acceptance/bundle/deploy/bind/orphaned-bind/databricks.yml new file mode 100644 index 00000000000..26d0272b267 --- /dev/null +++ b/acceptance/bundle/deploy/bind/orphaned-bind/databricks.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-orphaned + +resources: + jobs: + foo: + name: test-job + environments: + - environment_key: default + spec: + client: "1" + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: ./hello.py + +targets: + default: + bind: + jobs: + bar: + id: "12345" diff --git a/acceptance/bundle/deploy/bind/orphaned-bind/hello.py b/acceptance/bundle/deploy/bind/orphaned-bind/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/deploy/bind/orphaned-bind/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/deploy/bind/orphaned-bind/out.test.toml b/acceptance/bundle/deploy/bind/orphaned-bind/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/orphaned-bind/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/orphaned-bind/output.txt b/acceptance/bundle/deploy/bind/orphaned-bind/output.txt new file mode 100644 index 00000000000..6ae6816b737 --- /dev/null +++ b/acceptance/bundle/deploy/bind/orphaned-bind/output.txt @@ -0,0 +1,7 @@ + +>>> musterr [CLI] bundle plan +Error: bind block references undefined resource "resources.jobs.bar"; define it in the resources section or remove the bind block + at targets.default.bind.jobs.bar + +Error: bind validation failed + diff --git a/acceptance/bundle/deploy/bind/orphaned-bind/script b/acceptance/bundle/deploy/bind/orphaned-bind/script new file mode 100644 index 00000000000..aeae9c12fa1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/orphaned-bind/script @@ -0,0 +1,2 @@ +# Import block references jobs.bar but only jobs.foo exists in resources +trace musterr $CLI bundle plan diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/databricks.yml b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/databricks.yml new file mode 100644 index 00000000000..b55e1aec196 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/databricks.yml @@ -0,0 +1,18 @@ +bundle: + name: test-bind-recreate-in-state + +resources: + pipelines: + foo: + name: test-pipeline + storage: /old/storage/path + libraries: + - notebook: + path: ./nb.sql + +targets: + default: + bind: + pipelines: + foo: + id: "PLACEHOLDER_PIPELINE_ID" diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/nb.sql b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/nb.sql new file mode 100644 index 00000000000..199ff507884 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/nb.sql @@ -0,0 +1,2 @@ +-- Databricks notebook source +select 1 diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/out.test.toml b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt new file mode 100644 index 00000000000..8fd7ce49d3f --- /dev/null +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt @@ -0,0 +1,38 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-recreate-in-state/default/files... +Bound and updated pipelines.foo +Files: 2 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound + +>>> musterr [CLI] bundle plan +Error: cannot bind resources.pipelines.foo: cannot recreate resource with bind block + +This would destroy and recreate the existing workspace resource, changing its ID. + +The following fields cannot be modified because they require resource recreation: + - storage (immutable) + +To resolve this issue, you have two options: + +1. Remove the problematic fields from your configuration to make this a bind-only operation: + + resources: + pipelines: + foo: + # Remove or comment out these fields: + # storage: ... + +2. Remove the bind block if you want to allow the resource to be recreated/updated: + + targets: + : + # Remove the bind block: + # bind: + # pipelines: + # foo: + # id: + + +Error: planning failed + diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/script b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/script new file mode 100644 index 00000000000..a0909528b1c --- /dev/null +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/script @@ -0,0 +1,15 @@ +# Create a pipeline whose storage matches the config, so the first deploy binds +# it cleanly (no recreate). +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 the databricks.yml with the actual pipeline ID +update_file.py databricks.yml 'PLACEHOLDER_PIPELINE_ID' "$NEW_PIPELINE_ID" + +# Deploy to bind the resource into state +trace $CLI bundle deploy --auto-approve + +# Change storage (immutable) while the bind block is still defined. The bind +# block must keep guarding against recreate even after the resource is in state. +update_file.py databricks.yml '/old/storage/path' '/new/storage/path' +trace musterr $CLI bundle plan diff --git a/acceptance/bundle/deploy/bind/recreate-blocked/databricks.yml b/acceptance/bundle/deploy/bind/recreate-blocked/databricks.yml new file mode 100644 index 00000000000..ddad62da6ad --- /dev/null +++ b/acceptance/bundle/deploy/bind/recreate-blocked/databricks.yml @@ -0,0 +1,18 @@ +bundle: + name: test-bind-recreate + +resources: + pipelines: + foo: + name: test-pipeline + storage: /new/storage/path + libraries: + - notebook: + path: ./nb.sql + +targets: + default: + bind: + pipelines: + foo: + id: "PLACEHOLDER_PIPELINE_ID" diff --git a/acceptance/bundle/deploy/bind/recreate-blocked/nb.sql b/acceptance/bundle/deploy/bind/recreate-blocked/nb.sql new file mode 100644 index 00000000000..199ff507884 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recreate-blocked/nb.sql @@ -0,0 +1,2 @@ +-- Databricks notebook source +select 1 diff --git a/acceptance/bundle/deploy/bind/recreate-blocked/out.test.toml b/acceptance/bundle/deploy/bind/recreate-blocked/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recreate-blocked/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/recreate-blocked/output.txt b/acceptance/bundle/deploy/bind/recreate-blocked/output.txt new file mode 100644 index 00000000000..168068d9ca0 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recreate-blocked/output.txt @@ -0,0 +1,32 @@ + +>>> musterr [CLI] bundle plan +Error: cannot bind resources.pipelines.foo: cannot recreate resource with bind block + +This would destroy and recreate the existing workspace resource, changing its ID. + +The following fields cannot be modified because they require resource recreation: + - storage (immutable) + +To resolve this issue, you have two options: + +1. Remove the problematic fields from your configuration to make this a bind-only operation: + + resources: + pipelines: + foo: + # Remove or comment out these fields: + # storage: ... + +2. Remove the bind block if you want to allow the resource to be recreated/updated: + + targets: + : + # Remove the bind block: + # bind: + # pipelines: + # foo: + # id: + + +Error: planning failed + diff --git a/acceptance/bundle/deploy/bind/recreate-blocked/script b/acceptance/bundle/deploy/bind/recreate-blocked/script new file mode 100644 index 00000000000..f28f6b94ac8 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recreate-blocked/script @@ -0,0 +1,9 @@ +# Create a pipeline with a different storage path +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 the databricks.yml with the actual pipeline ID +update_file.py databricks.yml 'PLACEHOLDER_PIPELINE_ID' "$NEW_PIPELINE_ID" + +# Run plan - should fail because changing storage requires recreate which is blocked for binds +trace musterr $CLI bundle plan diff --git a/acceptance/bundle/deploy/bind/resource-not-found/databricks.yml b/acceptance/bundle/deploy/bind/resource-not-found/databricks.yml new file mode 100644 index 00000000000..e02258511da --- /dev/null +++ b/acceptance/bundle/deploy/bind/resource-not-found/databricks.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-not-found + +resources: + jobs: + foo: + name: test-job + environments: + - environment_key: default + spec: + client: "1" + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: ./hello.py + +targets: + default: + bind: + jobs: + foo: + id: "999999999" diff --git a/acceptance/bundle/deploy/bind/resource-not-found/hello.py b/acceptance/bundle/deploy/bind/resource-not-found/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/deploy/bind/resource-not-found/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/deploy/bind/resource-not-found/out.test.toml b/acceptance/bundle/deploy/bind/resource-not-found/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/resource-not-found/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/resource-not-found/output.txt b/acceptance/bundle/deploy/bind/resource-not-found/output.txt new file mode 100644 index 00000000000..bf1e85e7591 --- /dev/null +++ b/acceptance/bundle/deploy/bind/resource-not-found/output.txt @@ -0,0 +1,6 @@ + +>>> musterr [CLI] bundle plan +Error: cannot bind resources.jobs.foo: resource with ID "999999999" does not exist in workspace + +Error: planning failed + diff --git a/acceptance/bundle/deploy/bind/resource-not-found/script b/acceptance/bundle/deploy/bind/resource-not-found/script new file mode 100644 index 00000000000..6226cb85916 --- /dev/null +++ b/acceptance/bundle/deploy/bind/resource-not-found/script @@ -0,0 +1,2 @@ +# Try to plan with a non-existent job ID - should fail +trace musterr $CLI bundle plan diff --git a/acceptance/bundle/deploy/bind/test.toml b/acceptance/bundle/deploy/bind/test.toml new file mode 100644 index 00000000000..f59fefe3e02 --- /dev/null +++ b/acceptance/bundle/deploy/bind/test.toml @@ -0,0 +1,2 @@ +Cloud = true +Ignore = [".databricks"] diff --git a/acceptance/bundle/deploy/bind/top-level-bind/databricks.yml b/acceptance/bundle/deploy/bind/top-level-bind/databricks.yml new file mode 100644 index 00000000000..67833116972 --- /dev/null +++ b/acceptance/bundle/deploy/bind/top-level-bind/databricks.yml @@ -0,0 +1,21 @@ +bundle: + name: test-bind-top-level + +bind: + jobs: + foo: + id: "123" + +resources: + jobs: + foo: + name: test-job + environments: + - environment_key: default + spec: + client: "1" + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: ./hello.py diff --git a/acceptance/bundle/deploy/bind/top-level-bind/hello.py b/acceptance/bundle/deploy/bind/top-level-bind/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/deploy/bind/top-level-bind/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/deploy/bind/top-level-bind/out.test.toml b/acceptance/bundle/deploy/bind/top-level-bind/out.test.toml new file mode 100644 index 00000000000..a927a5fbc06 --- /dev/null +++ b/acceptance/bundle/deploy/bind/top-level-bind/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/top-level-bind/output.txt b/acceptance/bundle/deploy/bind/top-level-bind/output.txt new file mode 100644 index 00000000000..ee8fd54ee74 --- /dev/null +++ b/acceptance/bundle/deploy/bind/top-level-bind/output.txt @@ -0,0 +1,12 @@ + +>>> [CLI] bundle validate +Warning: unknown field: bind + in databricks.yml:4:1 + +Name: test-bind-top-level +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bind-top-level/default + +Found 1 warning diff --git a/acceptance/bundle/deploy/bind/top-level-bind/script b/acceptance/bundle/deploy/bind/top-level-bind/script new file mode 100644 index 00000000000..fd58a501146 --- /dev/null +++ b/acceptance/bundle/deploy/bind/top-level-bind/script @@ -0,0 +1,3 @@ +# A bind block at the root level (outside a target) is not recognized as a +# config field, so it surfaces as an "unknown field" warning and is ignored. +trace $CLI bundle validate diff --git a/acceptance/bundle/deploy/bind/top-level-bind/test.toml b/acceptance/bundle/deploy/bind/top-level-bind/test.toml new file mode 100644 index 00000000000..18b1a88417e --- /dev/null +++ b/acceptance/bundle/deploy/bind/top-level-bind/test.toml @@ -0,0 +1 @@ +Cloud = false diff --git a/acceptance/test.toml b/acceptance/test.toml index 6e18a40e78a..593f9961c60 100644 --- a/acceptance/test.toml +++ b/acceptance/test.toml @@ -23,7 +23,7 @@ Env.PYTHONUTF8 = "1" # applied in parallel, so the order of these lines is not deterministic. Sort each run of # them before comparing, so tests pin which resources were applied, not the order. # A test that needs the raw order sets SortLinesOn.deploy_actions = false. -SortLines.deploy_actions = '^(Created|Updated|Deleted|Recreated|Resized) \S+$' +SortLines.deploy_actions = '^(Created|Updated|Deleted|Recreated|Resized|Bound|Bound and updated) \S+$' # Retries are timing dependent, so their warnings would make output flaky. They go to the # test log instead. A test that injects errors deterministically and asserts the warning diff --git a/bundle/bundle.go b/bundle/bundle.go index 6762b38b91e..f8faaf85cf5 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -462,3 +462,11 @@ func (b *Bundle) IsImmutableFolder() bool { func (b *Bundle) IsSourceLinkedDeployment() bool { return b.SyncMode() == config.SyncModeInPlace } + +// BindConfig returns the selected target's bind blocks, or nil when no target is selected. +func (b *Bundle) BindConfig() config.Bind { + if b.Target == nil { + return nil + } + return b.Target.Bind +} diff --git a/bundle/config/bind.go b/bundle/config/bind.go new file mode 100644 index 00000000000..1e8ec9aa9d4 --- /dev/null +++ b/bundle/config/bind.go @@ -0,0 +1,73 @@ +package config + +import ( + "fmt" + "slices" + + "github.com/databricks/cli/libs/diag" +) + +// BindResource represents a single resource to bind with its workspace ID. +type BindResource struct { + ID string `json:"id"` +} + +// Bind defines existing workspace resources to bring under bundle management at deploy +// time. The outer map key is the resource type (e.g. "jobs", "pipelines"), the inner +// key is the resource name in the bundle config, and the value carries the workspace +// resource ID. Bind blocks are only valid for the direct deployment engine. +type Bind map[string]map[string]BindResource + +// ForEach calls fn for each bind entry in the configuration. Iteration order is +// stable (sorted by resource type, then resource name) so callers that emit user- +// visible diagnostics get deterministic output across runs. +func (b Bind) ForEach(fn func(resourceType, resourceName, bindID string)) { + resourceTypes := make([]string, 0, len(b)) + for resourceType := range b { + resourceTypes = append(resourceTypes, resourceType) + } + slices.Sort(resourceTypes) + for _, resourceType := range resourceTypes { + resources := b[resourceType] + names := make([]string, 0, len(resources)) + for name := range resources { + names = append(names, name) + } + slices.Sort(names) + for _, name := range names { + fn(resourceType, name, resources[name].ID) + } + } +} + +// IsEmpty returns true if no binds are defined. +func (b Bind) IsEmpty() bool { + for _, resources := range b { + if len(resources) > 0 { + return false + } + } + return true +} + +// Validate rejects bind blocks whose resource type is not a supported bundle +// resource. Only the resource types that can appear under the resources block are +// bindable; anything else (a typo, a child key like "jobs.permissions", etc.) is +// reported here rather than surfacing later as a confusing planning error. +func (b Bind) Validate() diag.Diagnostics { + var diags diag.Diagnostics + + supported := SupportedResources() + b.ForEach(func(resourceType, resourceName, bindID string) { + if _, ok := supported[resourceType]; ok { + return + } + diags = diags.Append(diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("binding %q is not supported", resourceType), + Detail: fmt.Sprintf("%q is not a supported resource type; bind is only supported for resources that can be defined under the resources block.", resourceType), + }) + }) + + return diags +} diff --git a/bundle/config/bind_test.go b/bundle/config/bind_test.go new file mode 100644 index 00000000000..88e2aacb9cf --- /dev/null +++ b/bundle/config/bind_test.go @@ -0,0 +1,70 @@ +package config_test + +import ( + "testing" + + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/libs/diag" + "github.com/stretchr/testify/assert" +) + +func TestBindIsEmpty(t *testing.T) { + cases := []struct { + name string + bind config.Bind + want bool + }{ + {"nil", nil, true}, + {"empty outer", config.Bind{}, true}, + {"empty inner", config.Bind{"jobs": {}}, true}, + {"populated", config.Bind{"jobs": {"foo": {ID: "1"}}}, false}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + assert.Equal(t, c.want, c.bind.IsEmpty()) + }) + } +} + +func TestBindForEachIteratesInSortedOrder(t *testing.T) { + bind := config.Bind{ + "pipelines": {"baz": {ID: "3"}}, + "jobs": {"foo": {ID: "1"}, "bar": {ID: "2"}}, + } + type entry struct{ rt, rn, id string } + var got []entry + bind.ForEach(func(rt, rn, id string) { + got = append(got, entry{rt, rn, id}) + }) + // ForEach guarantees stable order so multi-error diagnostics are reproducible. + assert.Equal(t, []entry{ + {"jobs", "bar", "2"}, + {"jobs", "foo", "1"}, + {"pipelines", "baz", "3"}, + }, got) +} + +func TestBindValidate(t *testing.T) { + cases := []struct { + name string + bind config.Bind + wantError bool + }{ + {"supported resource", config.Bind{"jobs": {"foo": {ID: "1"}}}, false}, + {"another supported resource", config.Bind{"pipelines": {"foo": {ID: "1"}}}, false}, + {"permissions child", config.Bind{"jobs.permissions": {"foo": {ID: "1"}}}, true}, + {"grants child", config.Bind{"schemas.grants": {"foo": {ID: "1"}}}, true}, + {"unknown resource type", config.Bind{"my_permissions_jobs": {"foo": {ID: "1"}}}, true}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + diags := c.bind.Validate() + if c.wantError { + assert.True(t, diags.HasError()) + assert.Equal(t, diag.Error, diags[0].Severity) + } else { + assert.False(t, diags.HasError()) + } + }) + } +} diff --git a/bundle/config/mutator/validate_bind_resources.go b/bundle/config/mutator/validate_bind_resources.go new file mode 100644 index 00000000000..129b90b3e49 --- /dev/null +++ b/bundle/config/mutator/validate_bind_resources.go @@ -0,0 +1,29 @@ +package mutator + +import ( + "context" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/diag" +) + +type validateBindResources struct{} + +// ValidateBindResources validates that bind blocks only contain valid resource types. +// Binding is only allowed for resources directly under the resources block, +// not for child resources like permissions or grants. +func ValidateBindResources() bundle.Mutator { + return &validateBindResources{} +} + +func (m *validateBindResources) Name() string { + return "ValidateBindResources" +} + +func (m *validateBindResources) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { + if b.Target == nil { + return nil + } + + return b.Target.Bind.Validate() +} diff --git a/bundle/config/mutator/validate_bind_resources_test.go b/bundle/config/mutator/validate_bind_resources_test.go new file mode 100644 index 00000000000..b3d5e63d799 --- /dev/null +++ b/bundle/config/mutator/validate_bind_resources_test.go @@ -0,0 +1,40 @@ +package mutator + +import ( + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/stretchr/testify/assert" +) + +func TestValidateBindResourcesNoTarget(t *testing.T) { + b := &bundle.Bundle{} + diags := bundle.Apply(t.Context(), b, ValidateBindResources()) + assert.NoError(t, diags.Error()) +} + +func TestValidateBindResourcesAcceptsTopLevel(t *testing.T) { + b := &bundle.Bundle{ + Target: &config.Target{ + Bind: config.Bind{ + "jobs": {"foo": {ID: "1"}}, + }, + }, + } + diags := bundle.Apply(t.Context(), b, ValidateBindResources()) + assert.NoError(t, diags.Error()) +} + +func TestValidateBindResourcesRejectsUnsupportedType(t *testing.T) { + b := &bundle.Bundle{ + Target: &config.Target{ + Bind: config.Bind{ + "jobs.permissions": {"foo": {ID: "1"}}, + }, + }, + } + diags := bundle.Apply(t.Context(), b, ValidateBindResources()) + assert.Error(t, diags.Error()) + assert.Contains(t, diags[0].Summary, `binding "jobs.permissions" is not supported`) +} diff --git a/bundle/config/target.go b/bundle/config/target.go index fae9c940b3f..a58e5191ee1 100644 --- a/bundle/config/target.go +++ b/bundle/config/target.go @@ -69,6 +69,11 @@ type Target struct { Sync *Sync `json:"sync,omitempty"` Permissions []resources.Permission `json:"permissions,omitempty"` + + // Bind specifies existing workspace resources to bind into bundle management. + // Resources listed here will be bound to the bundle at deploy time. + // This field is only valid for the direct deployment engine. + Bind Bind `json:"bind,omitempty"` } const ( diff --git a/bundle/deployplan/action.go b/bundle/deployplan/action.go index e53ef0d84bd..d74dada17e1 100644 --- a/bundle/deployplan/action.go +++ b/bundle/deployplan/action.go @@ -43,36 +43,45 @@ type ActionType string // If case of several options, action with highest severity wins. // Note, Create/Delete are handled explicitly and never compared. const ( - Undefined ActionType = "" - Skip ActionType = "skip" - Resize ActionType = "resize" - Update ActionType = "update" - UpdateWithID ActionType = "update_id" - Create ActionType = "create" - Recreate ActionType = "recreate" - Delete ActionType = "delete" + Undefined ActionType = "" + Skip ActionType = "skip" + Resize ActionType = "resize" + Update ActionType = "update" + UpdateWithID ActionType = "update_id" + Bind ActionType = "bind" + BindAndUpdate ActionType = "bind_and_update" + Create ActionType = "create" + Recreate ActionType = "recreate" + Delete ActionType = "delete" ) var actionOrder = map[ActionType]int{ - Undefined: 0, - Skip: 1, - Resize: 2, - Update: 3, - UpdateWithID: 4, - Create: 5, - Recreate: 6, - Delete: 7, + Undefined: 0, + Skip: 1, + Resize: 2, + Update: 3, + UpdateWithID: 4, + Bind: 5, + BindAndUpdate: 6, + Create: 7, + Recreate: 8, + Delete: 9, } func (a ActionType) KeepsID() bool { switch a { - case Create, UpdateWithID, Recreate, Delete: + case Create, UpdateWithID, Recreate, Delete, Bind, BindAndUpdate: return false default: return true } } +// IsBind returns true if the action is a bind action. +func (a ActionType) IsBind() bool { + return a == Bind || a == BindAndUpdate +} + // StringShort short version of action string, without suffix func (a ActionType) StringShort() string { items := strings.SplitN(string(a), "_", 2) @@ -85,8 +94,16 @@ func (a ActionType) StringShort() string { // sentence case of other output. "bundle plan" keeps the lower-case present tense, // so the two are still distinguishable at a glance. func AppliedLine(resourceKey string, action ActionType) string { + key := strings.TrimPrefix(resourceKey, "resources.") + // "bind" does not follow the "+d" rule, and bind_and_update names both halves. + switch action { + case Bind: + return "Bound " + key + case BindAndUpdate: + return "Bound and updated " + key + } verb := action.StringShort() + "d" - return strings.ToUpper(verb[:1]) + verb[1:] + " " + strings.TrimPrefix(resourceKey, "resources.") + return strings.ToUpper(verb[:1]) + verb[1:] + " " + key } // GetHigherAction returns the action with higher severity between a and b. diff --git a/bundle/deployplan/plan.go b/bundle/deployplan/plan.go index 8446b200b12..1591ac2d770 100644 --- a/bundle/deployplan/plan.go +++ b/bundle/deployplan/plan.go @@ -49,6 +49,9 @@ type ActionCounts struct { Change int Delete int Unchanged int + // Bind counts bind and bind_and_update actions; a bind_and_update also + // counts as a change, since it applies config to the adopted resource. + Bind int } // CountActions tallies the plan's actions by category. Order is irrelevant to a @@ -74,6 +77,11 @@ func (p *Plan) CountActions() ActionCounts { // A recreate counts as both a delete and a create. c.Delete++ c.Create++ + case Bind: + c.Bind++ + case BindAndUpdate: + c.Bind++ + c.Change++ case Skip, Undefined: c.Unchanged++ } @@ -120,6 +128,7 @@ func LoadPlanFromFile(path string) (*Plan, error) { type PlanEntry struct { ID string `json:"id,omitempty"` + BindID string `json:"bind_id,omitempty"` DependsOn []DependsOnEntry `json:"depends_on,omitempty"` Action ActionType `json:"action,omitempty"` // Gone is set on Delete entries when planning confirmed the resource no longer diff --git a/bundle/direct/apply.go b/bundle/direct/apply.go index 9cdd5a4f505..04486d879d0 100644 --- a/bundle/direct/apply.go +++ b/bundle/direct/apply.go @@ -36,8 +36,13 @@ func (d *DeploymentUnit) Destroy(ctx context.Context, db *dstate.DeploymentState func (d *DeploymentUnit) Deploy(ctx context.Context, db *dstate.DeploymentState, newState any, actionType deployplan.ActionType, planEntry *deployplan.PlanEntry) error { ctx = log.WithPrefix(ctx, "deploying "+d.ResourceKey) ctx = d.withResourceKey(ctx) - if actionType == deployplan.Create { + switch actionType { + case deployplan.Create: return d.Create(ctx, db, newState) + case deployplan.Bind: + return d.Bind(ctx, db, planEntry.BindID, newState) + case deployplan.BindAndUpdate: + return d.BindAndUpdate(ctx, db, planEntry.BindID, newState, planEntry) } oldID := db.GetResourceID(d.ResourceKey) @@ -59,6 +64,42 @@ func (d *DeploymentUnit) Deploy(ctx context.Context, db *dstate.DeploymentState, } } +// Bind brings an existing workspace resource under bundle management by persisting +// its ID to state, without modifying the remote resource. +func (d *DeploymentUnit) Bind(ctx context.Context, db *dstate.DeploymentState, id string, newState any) error { + err := db.SaveState(ctx, d.ResourceKey, id, newState, d.DependsOn) + if err != nil { + return fmt.Errorf("saving state id=%s: %w", id, err) + } + + log.Infof(ctx, "Bound %s id=%s", d.ResourceKey, id) + return nil +} + +// BindAndUpdate binds an existing workspace resource and applies the configured +// field changes to it. It dispatches on the planned action so the change goes +// through the same path a normal deploy would: Resize for running-cluster +// worker/autoscale drift, Update otherwise. Recreate / UpdateWithID are rejected +// at plan time, so only Update and Resize can reach here. +func (d *DeploymentUnit) BindAndUpdate(ctx context.Context, db *dstate.DeploymentState, id string, newState any, planEntry *deployplan.PlanEntry) error { + action := getMaxAction(planEntry.Changes) + switch action { + case deployplan.Resize: + if err := d.Resize(ctx, db, id, newState, planEntry); err != nil { + return err + } + case deployplan.Update: + if err := d.Update(ctx, db, id, newState, planEntry); err != nil { + return err + } + default: + return fmt.Errorf("internal error: unexpected action %q during bind_and_update", action) + } + + log.Infof(ctx, "Bound and updated %s id=%s", d.ResourceKey, id) + return nil +} + // Create creates the resource and records its state. func (d *DeploymentUnit) Create(ctx context.Context, db *dstate.DeploymentState, newState any) error { var newID string diff --git a/bundle/direct/bind.go b/bundle/direct/bind.go index 7fbf7ede662..522b4b336e7 100644 --- a/bundle/direct/bind.go +++ b/bundle/direct/bind.go @@ -124,7 +124,7 @@ func (b *DeploymentBundle) Bind(ctx context.Context, client *databricks.Workspac os.Remove(tmpStatePath) return nil, err } - plan, err := b.CalculatePlan(ctx, client, configRoot) + plan, err := b.CalculatePlan(ctx, client, configRoot, nil) if err != nil { os.Remove(tmpStatePath) return nil, err @@ -197,7 +197,7 @@ func (b *DeploymentBundle) Bind(ctx context.Context, client *databricks.Workspac os.Remove(tmpStatePath) return nil, err } - plan, err = b.CalculatePlan(ctx, client, configRoot) + plan, err = b.CalculatePlan(ctx, client, configRoot, nil) if _, ferr := b.StateDB.Finalize(ctx); ferr != nil { log.Warnf(ctx, "failed to finalize state: %v", ferr) } diff --git a/bundle/direct/bundle_plan.go b/bundle/direct/bundle_plan.go index 8f8336beaa5..19c9a2420c4 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -161,7 +161,11 @@ func (b *DeploymentBundle) StampDeploymentIdForFirstVersion(deploymentID string) // CalculatePlan computes the deployment plan by comparing local config against remote state. // StateDB must already be open for read before calling this function. -func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks.WorkspaceClient, configRoot *config.Root) (*deployplan.Plan, error) { +// +// bindConfig carries the target's declarative bind blocks; resources listed there +// that are not yet in state are planned as Bind / BindAndUpdate instead of Create. +// Pass an empty Bind when there is nothing to bind (e.g. destroy). +func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks.WorkspaceClient, configRoot *config.Root, bindConfig config.Bind) (*deployplan.Plan, error) { b.StateDB.AssertOpenedForRead() err := b.init(client) @@ -181,6 +185,19 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks b.Plan = plan + // Validate bind blocks against the full plan and state before walking the graph, + // then index them by resource key so the per-resource planning below can select + // Bind / BindAndUpdate for resources that are being bound for the first time. + bindIDByKey := map[string]string{} + if !bindConfig.IsEmpty() { + if !b.validateBindConfig(ctx, configRoot, bindConfig) { + return nil, errors.New("bind validation failed") + } + bindConfig.ForEach(func(resourceType, resourceName, bindID string) { + bindIDByKey["resources."+resourceType+"."+resourceName] = bindID + }) + } + g, err := makeGraph(plan) if err != nil { return nil, err @@ -275,6 +292,14 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks // state files may still carry a malformed entry). Treat as missing // and let the resource be re-created on this plan. if !hasEntry || dbentry.ID == "" { + // A resource that is not in state but carries a bind block is imported + // rather than created: read the remote resource under the bound ID and + // plan a Bind / BindAndUpdate instead of a Create. + if bindID, ok := bindIDByKey[resourceKey]; ok { + entry.BindID = bindID + b.handleBindPlan(ctx, resourceKey, entry, adapter) + return !logdiag.HasError(ctx) + } entry.Action = deployplan.Create return true } @@ -382,6 +407,15 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks // it can be used for variable resolution depends on several factors, see canReadRemoteCache in LookupReferencePreDeploy b.RemoteStateCache.Store(resourceKey, remoteState) + // An active bind block guards its resource against recreation for as long as + // it is defined, not just during the first bind. Once the resource is in + // state, a config change that would recreate or re-ID it still surfaces as an + // error so the bind block behaves as a persistent guard (see handleBindPlan). + if _, bound := bindIDByKey[resourceKey]; bound && (action == deployplan.Recreate || action == deployplan.UpdateWithID) { + logdiag.LogError(ctx, buildBindConflictError("cannot bind "+resourceKey, action, entry.Changes, resourceKey)) + return false + } + // Validate that resources without DoUpdate don't have update actions if action == deployplan.Update && !adapter.HasDoUpdate() { logdiag.LogError(ctx, fmt.Errorf("%s: resource does not support update action but plan produced update", errorPrefix)) diff --git a/bundle/direct/bundle_plan_bind.go b/bundle/direct/bundle_plan_bind.go new file mode 100644 index 00000000000..bde6f3931bd --- /dev/null +++ b/bundle/direct/bundle_plan_bind.go @@ -0,0 +1,276 @@ +package direct + +import ( + "context" + "errors" + "fmt" + "slices" + "strings" + + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/deployplan" + "github.com/databricks/cli/bundle/direct/dresources" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/logdiag" + "github.com/databricks/cli/libs/retry" + "github.com/databricks/cli/libs/structs/structdiff" + "github.com/databricks/cli/libs/structs/structpath" + "github.com/databricks/databricks-sdk-go/apierr" +) + +// bindBlockPath returns the config path of a bind block, used to point +// diagnostics at the offending entry. +func bindBlockPath(targetName, resourceType, resourceName string) *structpath.PathNode { + return structpath.NewPath(nil, "targets", targetName, "bind", resourceType, resourceName) +} + +// validateBindConfig logs diagnostics for invalid bind entries. Returns false if any +// errors were logged. +func (b *DeploymentBundle) validateBindConfig(ctx context.Context, configRoot *config.Root, bindConfig config.Bind) bool { + hasError := false + targetName := configRoot.Bundle.Target + + // Bind blocks must reference resources that exist in the merged config. + bindConfig.ForEach(func(resourceType, resourceName, bindID string) { + key := "resources." + resourceType + "." + resourceName + if _, ok := b.Plan.Plan[key]; ok { + return + } + bindPath := bindBlockPath(targetName, resourceType, resourceName) + logdiag.LogDiag(ctx, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("bind block references undefined resource %q; define it in the resources section or remove the bind block", key), + Locations: configRoot.GetLocationsOf(bindPath), + Paths: []*structpath.PathNode{bindPath}, + }) + hasError = true + }) + + // A resource that is already in state cannot be re-bound to a different ID: the + // bind block is meant to import a resource for the first time. When the IDs match + // the bind is a no-op and the resource just deploys normally. + bindConfig.ForEach(func(resourceType, resourceName, bindID string) { + key := "resources." + resourceType + "." + resourceName + dbentry, hasEntry := b.StateDB.GetResourceEntry(key) + if !hasEntry || dbentry.ID == "" || dbentry.ID == bindID { + return + } + bindPath := bindBlockPath(targetName, resourceType, resourceName) + logdiag.LogDiag(ctx, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("%s: resource already bound to ID %q, cannot bind as %q; remove the bind block or unbind the existing resource", key, dbentry.ID, bindID), + Locations: configRoot.GetLocationsOf(bindPath), + Paths: []*structpath.PathNode{bindPath}, + }) + hasError = true + }) + + // Two bind blocks must not point at the same workspace ID. Otherwise both would + // be bound for the first time and create two state entries managing one remote + // resource; reject the collision instead of silently double-managing it. + bindKeysByID := map[string][]string{} + bindConfig.ForEach(func(resourceType, resourceName, bindID string) { + bindKey := "resources." + resourceType + "." + resourceName + bindKeysByID[bindID] = append(bindKeysByID[bindID], bindKey) + }) + bindConfig.ForEach(func(resourceType, resourceName, bindID string) { + keys := bindKeysByID[bindID] + if len(keys) < 2 { + return + } + bindKey := "resources." + resourceType + "." + resourceName + others := slices.DeleteFunc(slices.Clone(keys), func(k string) bool { return k == bindKey }) + bindPath := bindBlockPath(targetName, resourceType, resourceName) + logdiag.LogDiag(ctx, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("bind block for %q has the same ID %q as bind block(s) for %s; each workspace resource can only be bound once", bindKey, bindID, strings.Join(others, ", ")), + Locations: configRoot.GetLocationsOf(bindPath), + Paths: []*structpath.PathNode{bindPath}, + }) + hasError = true + }) + + // A bind ID must not collide with another resource's ID in state. Such a collision + // would let a bind silently redirect a delete/recreate/update_id at a resource the + // user is also trying to import; reject it instead of choosing one over the other. + stateKeys := make([]string, 0, len(b.StateDB.Data.State)) + for stateKey := range b.StateDB.Data.State { + stateKeys = append(stateKeys, stateKey) + } + slices.Sort(stateKeys) + + bindConfig.ForEach(func(resourceType, resourceName, bindID string) { + bindKey := "resources." + resourceType + "." + resourceName + for _, stateKey := range stateKeys { + stateEntry := b.StateDB.Data.State[stateKey] + if stateKey == bindKey || stateEntry.ID != bindID { + continue + } + bindPath := bindBlockPath(targetName, resourceType, resourceName) + logdiag.LogDiag(ctx, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("bind block for %q has the same ID %q as existing resource %q; remove the bind block or the conflicting resource", bindKey, bindID, stateKey), + Locations: configRoot.GetLocationsOf(bindPath), + Paths: []*structpath.PathNode{bindPath}, + }) + hasError = true + } + }) + + return !hasError +} + +// handleBindPlan reads the remote resource for a resource that is being bound for +// the first time, computes the change set against the local config, and selects +// either Bind (no changes) or BindAndUpdate (config drift can be applied). +// +// Recreate / UpdateWithID actions imply destroying or replacing the workspace +// resource — incompatible with bind — so they surface as actionable errors. +func (b *DeploymentBundle) handleBindPlan(ctx context.Context, resourceKey string, entry *deployplan.PlanEntry, adapter *dresources.Adapter) { + bindID := entry.BindID + errorPrefix := "cannot bind " + resourceKey + + remoteState, err := retry.With(ctx, retry.IsTransientRead, func() (any, error) { + return adapter.DoRead(ctx, bindID) + }) + if err != nil { + if apierr.IsMissing(err) { + logdiag.LogError(ctx, fmt.Errorf("%s: resource with ID %q does not exist in workspace", errorPrefix, bindID)) + } else { + logdiag.LogError(ctx, fmt.Errorf("%s: reading remote resource id=%q: %w", errorPrefix, bindID, err)) + } + return + } + + entry.RemoteState = remoteState + b.RemoteStateCache.Store(resourceKey, remoteState) + + sv, ok := b.StateCache.Load(resourceKey) + if !ok { + logdiag.LogError(ctx, fmt.Errorf("%s: internal error: no state cache entry", errorPrefix)) + return + } + + remoteStateComparable, err := adapter.RemapState(remoteState) + if err != nil { + logdiag.LogError(ctx, fmt.Errorf("%s: interpreting remote state: %w", errorPrefix, err)) + return + } + + // Compact both sides on the same fields, so a hashed_fields field is a hash on + // both sides of the diff. sv.Value keeps the full contents for the deploy. + remoteStateComparable, err = dresources.CompactState(adapter.ResourceConfig(), remoteStateComparable) + if err != nil { + logdiag.LogError(ctx, fmt.Errorf("%s: compacting remote state: %w", errorPrefix, err)) + return + } + localState, err := dresources.CompactState(adapter.ResourceConfig(), sv.Value) + if err != nil { + logdiag.LogError(ctx, fmt.Errorf("%s: compacting local state: %w", errorPrefix, err)) + return + } + + remoteDiff, err := structdiff.GetStructDiff(remoteStateComparable, localState, adapter.KeyedSlices()) + if err != nil { + logdiag.LogError(ctx, fmt.Errorf("%s: diffing remote state: %w", errorPrefix, err)) + return + } + + // No "saved state" exists for a first-time bind; the diff is purely remote vs. config. + entry.Changes, err = prepareChanges(ctx, adapter, nil, remoteDiff, nil, remoteStateComparable) + if err != nil { + logdiag.LogError(ctx, fmt.Errorf("%s: %w", errorPrefix, err)) + return + } + + err = addPerFieldActions(ctx, adapter, entry.Changes, sv.Value, remoteState) + if err != nil { + logdiag.LogError(ctx, fmt.Errorf("%s: classifying changes: %w", errorPrefix, err)) + return + } + + switch maxAction := getMaxAction(entry.Changes); maxAction { + case deployplan.Skip, deployplan.Undefined: + entry.Action = deployplan.Bind + case deployplan.Update, deployplan.Resize: + entry.Action = deployplan.BindAndUpdate + case deployplan.Recreate, deployplan.UpdateWithID: + logdiag.LogError(ctx, buildBindConflictError(errorPrefix, maxAction, entry.Changes, resourceKey)) + default: + logdiag.LogError(ctx, fmt.Errorf("%s: internal error: unexpected action %q during bind planning", errorPrefix, maxAction)) + } +} + +// buildBindConflictError formats a multi-line message that names the fields whose +// changes forced an incompatible action (Recreate / UpdateWithID) and shows the two +// concrete resolutions: drop the offending fields, or drop the bind block. +func buildBindConflictError(errorPrefix string, action deployplan.ActionType, changes deployplan.Changes, resourceKey string) error { + problematicFields := make([]string, 0, len(changes)) + for fieldPath, change := range changes { + if change.Action == action { + problematicFields = append(problematicFields, fieldPath) + } + } + // Stable order so the YAML examples are deterministic across runs. + slices.Sort(problematicFields) + + resourceType, resourceName := splitResourceKey(resourceKey) + + var msg strings.Builder + // buildBindConflictError is only invoked for Recreate / UpdateWithID; other + // action types are guarded by the caller. + switch action { //nolint:exhaustive + case deployplan.Recreate: + msg.WriteString(errorPrefix + ": cannot recreate resource with bind block\n\n") + msg.WriteString("This would destroy and recreate the existing workspace resource, changing its ID.\n\n") + case deployplan.UpdateWithID: + msg.WriteString(errorPrefix + ": cannot update resource ID with bind block\n\n") + msg.WriteString("This would replace the existing workspace resource with a new ID.\n\n") + } + + msg.WriteString("The following fields cannot be modified because they require ") + if action == deployplan.Recreate { + msg.WriteString("resource recreation:\n") + } else { + msg.WriteString("ID changes:\n") + } + for _, field := range problematicFields { + if reason := changes[field].Reason; reason != "" { + fmt.Fprintf(&msg, " - %s (%s)\n", field, reason) + } else { + fmt.Fprintf(&msg, " - %s\n", field) + } + } + msg.WriteString("\n") + + msg.WriteString("To resolve this issue, you have two options:\n\n") + msg.WriteString("1. Remove the problematic fields from your configuration to make this a bind-only operation:\n\n") + msg.WriteString(" resources:\n") + fmt.Fprintf(&msg, " %s:\n", resourceType) + fmt.Fprintf(&msg, " %s:\n", resourceName) + msg.WriteString(" # Remove or comment out these fields:\n") + for _, field := range problematicFields { + fmt.Fprintf(&msg, " # %s: ...\n", field) + } + msg.WriteString("\n") + msg.WriteString("2. Remove the bind block if you want to allow the resource to be recreated/updated:\n\n") + msg.WriteString(" targets:\n") + msg.WriteString(" :\n") + msg.WriteString(" # Remove the bind block:\n") + msg.WriteString(" # bind:\n") + fmt.Fprintf(&msg, " # %s:\n", resourceType) + fmt.Fprintf(&msg, " # %s:\n", resourceName) + msg.WriteString(" # id: \n") + + return errors.New(msg.String()) +} + +// splitResourceKey extracts the resource type and name from a key like +// "resources.jobs.foo" → ("jobs", "foo"). +func splitResourceKey(resourceKey string) (resourceType, resourceName string) { + parts := strings.SplitN(resourceKey, ".", 3) + if len(parts) < 3 { + return "", "" + } + return parts[1], parts[2] +} diff --git a/bundle/internal/schema/annotations.yml b/bundle/internal/schema/annotations.yml index f983a1a99b7..551c4beef10 100644 --- a/bundle/internal/schema/annotations.yml +++ b/bundle/internal/schema/annotations.yml @@ -2263,6 +2263,13 @@ targets: "artifacts": "description": |- The artifacts to include in the target deployment. + "bind": + "description": |- + The existing workspace resources to bind into bundle management for this target. + "$fields": + "id": + "description": |- + The ID of the existing workspace resource to bind. "bundle": "description": |- The bundle attributes when deploying to this target. diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index c3c3306130e..7e1337796c2 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -132,6 +132,9 @@ func logDeploySummary(ctx context.Context, b *bundle.Bundle, plan *deployplan.Pl counts := plan.CountActions() summary := fmt.Sprintf("Resources: %d created, %d changed, %d deleted, %d unchanged", counts.Create, counts.Change, counts.Delete, counts.Unchanged) + if counts.Bind > 0 { + summary += fmt.Sprintf(", %d bound", counts.Bind) + } // Gate on the plan's own NotSelected (not b.Select) so the suffix survives a // deploy from a --plan file, where --select was applied at plan time and // b.Select is empty here. NotSelected is only ever set by FilterToSelected. @@ -355,7 +358,7 @@ func Deploy(ctx context.Context, b *bundle.Bundle, outputHandler sync.OutputHand } func RunPlan(ctx context.Context, b *bundle.Bundle) *deployplan.Plan { - plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config) + plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config, b.BindConfig()) if err != nil { logdiag.LogError(ctx, err) return nil diff --git a/bundle/phases/destroy.go b/bundle/phases/destroy.go index b3e885e54d4..3cfa7a888c1 100644 --- a/bundle/phases/destroy.go +++ b/bundle/phases/destroy.go @@ -322,7 +322,12 @@ func Destroy(ctx context.Context, b *bundle.Bundle) { bundle.ApplyContext(ctx, b, lock.Release(lock.GoalDestroy)) }() - plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), nil) + // bundle destroy is allowed even when bind blocks reference resources in + // state: naming destroy explicitly captures the user's intent to tear the + // deployment down. The bind block only guards against accidental deletion + // during a normal deploy (a bound resource dropped from config or a change + // that would recreate it), not against a deliberate destroy. + plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), nil, nil) if err != nil { logdiag.LogError(ctx, err) return diff --git a/bundle/phases/dms.go b/bundle/phases/dms.go index 38210913ce8..3b13bb14ee8 100644 --- a/bundle/phases/dms.go +++ b/bundle/phases/dms.go @@ -63,6 +63,10 @@ func actionToSDK(a deployplan.ActionType) (bundledeployments.OperationActionType return bundledeployments.OperationActionTypeOperationActionTypeResize, nil case deployplan.Delete: return bundledeployments.OperationActionTypeOperationActionTypeDelete, nil + case deployplan.Bind: + return bundledeployments.OperationActionTypeOperationActionTypeBind, nil + case deployplan.BindAndUpdate: + return bundledeployments.OperationActionTypeOperationActionTypeBindAndUpdate, nil default: return "", fmt.Errorf("cannot record operation: unsupported action %q", a) } diff --git a/bundle/phases/initialize.go b/bundle/phases/initialize.go index c855935598f..4fe3dc86bfb 100644 --- a/bundle/phases/initialize.go +++ b/bundle/phases/initialize.go @@ -255,6 +255,10 @@ func Initialize(ctx context.Context, b *bundle.Bundle) { // Validates that when using a shared workspace path, appropriate permissions are configured permissions.ValidateSharedRootPermissions(), + // Reads (typed): b.Config.Targets..Bind (checks for permissions/grants under bind, which are not bindable) + // Reports validation errors when bind blocks reference child resources instead of top-level resources. + mutator.ValidateBindResources(), + // Annotate resources with "deployment" metadata. // // We don't include this step into initializeResources because these mutators set fields that are diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index 93c4d40e6a3..c875eaedf8a 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -3949,6 +3949,27 @@ "config.ArtifactType": { "type": "string" }, + "config.BindResource": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "description": "The ID of the existing workspace resource to bind.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false, + "required": [ + "id" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "config.Bundle": { "oneOf": [ { @@ -4490,6 +4511,10 @@ "description": "The artifacts to include in the target deployment.", "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config.Artifact" }, + "bind": { + "description": "The existing workspace resources to bind into bundle management for this target.", + "$ref": "#/$defs/map/map/github.com/databricks/cli/bundle/config.BindResource" + }, "bundle": { "description": "The bundle attributes when deploying to this target.", "$ref": "#/$defs/github.com/databricks/cli/bundle/config.Bundle" @@ -22100,6 +22125,20 @@ } ] }, + "config.BindResource": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/github.com/databricks/cli/bundle/config.BindResource" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "config.Command": { "oneOf": [ { @@ -22146,6 +22185,30 @@ } } }, + "map": { + "github.com": { + "databricks": { + "cli": { + "bundle": { + "config.BindResource": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config.BindResource" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + } + } + } + } + } + }, "string": { "oneOf": [ { diff --git a/bundle/statemgmt/direct_migration.go b/bundle/statemgmt/direct_migration.go index 4fefd8504ac..2668ecb0835 100644 --- a/bundle/statemgmt/direct_migration.go +++ b/bundle/statemgmt/direct_migration.go @@ -200,7 +200,8 @@ func checkPlanOnTempState(ctx context.Context, b *bundle.Bundle, tempStatePath s return nil, fmt.Errorf("opening migrated state for plan check: %w", err) } - return planBundle.CalculatePlan(planCtx, b.WorkspaceClient(ctx), cfg) + // Pass the bind blocks so the check plans what the deploy will plan. + return planBundle.CalculatePlan(planCtx, b.WorkspaceClient(ctx), cfg, b.BindConfig()) } // recreatedResources returns the sorted keys of resources the plan would recreate diff --git a/cmd/bundle/config_remote_sync.go b/cmd/bundle/config_remote_sync.go index 67f74e926bf..9285f806ee2 100644 --- a/cmd/bundle/config_remote_sync.go +++ b/cmd/bundle/config_remote_sync.go @@ -90,7 +90,7 @@ Examples: // ProcessBundleRet has already opened the direct state for reading. deployBundle := &b.DeploymentBundle - plan, err := deployBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config) + plan, err := deployBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config, nil) if err != nil { stats.ErrorCategory = protos.BundleConfigRemoteSyncErrorCategoryDetectChangesFailed return fmt.Errorf("failed to detect changes: %w", err) diff --git a/cmd/bundle/deployment/migrate.go b/cmd/bundle/deployment/migrate.go index f329ec7962c..f2f4a219668 100644 --- a/cmd/bundle/deployment/migrate.go +++ b/cmd/bundle/deployment/migrate.go @@ -89,6 +89,14 @@ to the workspace so that subsequent deploys of this bundle use direct deployment } ctx := cmd.Context() + // `bundle deployment migrate` runs against an existing terraform deployment to + // produce a direct-engine state. Bind is a direct-engine-only feature, so we + // don't have a defined behavior for "migrating" with bind blocks present. Reject + // it and let the user remove the bind blocks, migrate, then add them back. + if b.Target != nil && !b.Target.Bind.IsEmpty() { + return errors.New("cannot run 'bundle deployment migrate' when bind blocks are defined in the target configuration; bind blocks are only supported with the direct deployment engine") + } + if stateDesc.Lineage == "" { cmdio.LogString(ctx, `Error: This command migrates the existing Terraform state file (terraform.tfstate) to a direct deployment state file (resources.json). However, no existing local or remote state was found. diff --git a/cmd/bundle/plan.go b/cmd/bundle/plan.go index 1132834bc4d..f7294a6e2a9 100644 --- a/cmd/bundle/plan.go +++ b/cmd/bundle/plan.go @@ -82,7 +82,7 @@ It is useful for previewing changes before running 'bundle deploy'.`, switch root.OutputType(cmd) { case flags.OutputText: // Print summary line and actions to stdout - totalChanges := counts.Create + counts.Change + counts.Delete + totalChanges := counts.Create + counts.Change + counts.Delete + counts.Bind if totalChanges > 0 && bundle.QuietLevel(quiet) < bundle.QuietSummary { // Print all actions in the order they were processed for _, action := range plan.GetActions() { @@ -95,7 +95,14 @@ It is useful for previewing changes before running 'bundle deploy'.`, continue } key := strings.TrimPrefix(action.ResourceKey, "resources.") - fmt.Fprintf(out, "%s %s\n", action.ActionType.StringShort(), key) + if action.ActionType.IsBind() { + // Print the full action string (not StringShort) so bind and + // bind_and_update are distinguishable in the text plan. + entry := plan.Plan[action.ResourceKey] + fmt.Fprintf(out, "%s %s (id: %s)\n", action.ActionType, key, entry.BindID) + } else { + fmt.Fprintf(out, "%s %s\n", action.ActionType.StringShort(), key) + } } fmt.Fprintln(out) } @@ -104,6 +111,9 @@ It is useful for previewing changes before running 'bundle deploy'.`, } // Note, this string should not be changed, "bundle deployment migrate" depends on this format: fmt.Fprintf(out, "Plan: %d to add, %d to change, %d to delete, %d unchanged", counts.Create, counts.Change, counts.Delete, counts.Unchanged) + if counts.Bind > 0 { + fmt.Fprintf(out, ", %d to bind", counts.Bind) + } if len(selectResources) > 0 { fmt.Fprintf(out, ", %d not selected", plan.NotSelected) } From e9b8ab7d840b40c7779856a3439d353cbb30f524 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:05:24 +0000 Subject: [PATCH 02/20] Reject bind blocks for resources missing from config at validation time A bind block whose resource is not in the resources section was only caught at plan time, by looking the key up in the plan map. State-only keys are in that map as delete entries, so removing a bound resource from config while keeping its bind block passed validation and 'bundle deploy' deleted the bound workspace resource. The agreed behaviour is that delete protection applies for as long as the bind block remains. Move the check into ValidateBindResources (initialize phase): it needs only config, so 'bundle validate' now reports it, and a delete can no longer be planned for a key that still has a bind block. Co-authored-by: Isaac --- .../bind/delete-with-block/databricks.yml | 23 ++++++++++ .../delete-with-block/databricks_nores.yml | 9 ++++ .../bind/delete-with-block/out.test.toml | 2 + .../deploy/bind/delete-with-block/output.txt | 29 ++++++++++++ .../deploy/bind/delete-with-block/script | 20 +++++++++ .../deploy/bind/delete-with-block/test.toml | 1 + .../deploy/bind/orphaned-bind/output.txt | 14 +++++- .../bundle/deploy/bind/orphaned-bind/script | 4 +- .../config/mutator/validate_bind_resources.go | 44 +++++++++++++++++-- .../mutator/validate_bind_resources_test.go | 28 +++++++++++- bundle/direct/bundle_plan_bind.go | 17 +------ 11 files changed, 168 insertions(+), 23 deletions(-) create mode 100644 acceptance/bundle/deploy/bind/delete-with-block/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/delete-with-block/databricks_nores.yml create mode 100644 acceptance/bundle/deploy/bind/delete-with-block/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/delete-with-block/output.txt create mode 100644 acceptance/bundle/deploy/bind/delete-with-block/script create mode 100644 acceptance/bundle/deploy/bind/delete-with-block/test.toml diff --git a/acceptance/bundle/deploy/bind/delete-with-block/databricks.yml b/acceptance/bundle/deploy/bind/delete-with-block/databricks.yml new file mode 100644 index 00000000000..768501ce455 --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-with-block/databricks.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-delete-with-block + +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: /Workspace/test.py + +targets: + default: + bind: + jobs: + foo: + id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/delete-with-block/databricks_nores.yml b/acceptance/bundle/deploy/bind/delete-with-block/databricks_nores.yml new file mode 100644 index 00000000000..ce29e0f57c7 --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-with-block/databricks_nores.yml @@ -0,0 +1,9 @@ +bundle: + name: test-bind-delete-with-block + +targets: + default: + bind: + jobs: + foo: + id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/delete-with-block/out.test.toml b/acceptance/bundle/deploy/bind/delete-with-block/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-with-block/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/delete-with-block/output.txt b/acceptance/bundle/deploy/bind/delete-with-block/output.txt new file mode 100644 index 00000000000..fc12312a182 --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-with-block/output.txt @@ -0,0 +1,29 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-delete-with-block/default/files... +Bound and updated jobs.foo +Files: 2 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound + +=== Remove the resource from config but keep its bind block: the block must prevent the delete +>>> musterr [CLI] bundle plan +Error: bind block references undefined resource "resources.jobs.foo"; define it in the resources section or remove the bind block + at targets.default.bind.jobs.foo + + +>>> musterr [CLI] bundle deploy --auto-approve +Error: bind block references undefined resource "resources.jobs.foo"; define it in the resources section or remove the bind block + at targets.default.bind.jobs.foo + + +=== The bound job still exists +>>> [CLI] jobs get [NEW_JOB_ID] +test-bind-job + +>>> [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-delete-with-block/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/delete-with-block/script b/acceptance/bundle/deploy/bind/delete-with-block/script new file mode 100644 index 00000000000..407b9c80149 --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-with-block/script @@ -0,0 +1,20 @@ +# Create a job in the workspace and bind it +NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" +update_file.py databricks_nores.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" + +trace $CLI bundle deploy --auto-approve + +title "Remove the resource from config but keep its bind block: the block must prevent the delete" +cp databricks.yml databricks.yml.bak +cp databricks_nores.yml databricks.yml +trace musterr $CLI bundle plan +trace musterr $CLI bundle deploy --auto-approve + +title "The bound job still exists" +trace $CLI jobs get $NEW_JOB_ID | jq -r .settings.name + +# Cleanup +cp databricks.yml.bak databricks.yml +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deploy/bind/delete-with-block/test.toml b/acceptance/bundle/deploy/bind/delete-with-block/test.toml new file mode 100644 index 00000000000..284fca57d4b --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-with-block/test.toml @@ -0,0 +1 @@ +Ignore = [".databricks", "databricks.yml.bak", "databricks_nores.yml"] diff --git a/acceptance/bundle/deploy/bind/orphaned-bind/output.txt b/acceptance/bundle/deploy/bind/orphaned-bind/output.txt index 6ae6816b737..197b527da3f 100644 --- a/acceptance/bundle/deploy/bind/orphaned-bind/output.txt +++ b/acceptance/bundle/deploy/bind/orphaned-bind/output.txt @@ -1,7 +1,17 @@ ->>> musterr [CLI] bundle plan +>>> musterr [CLI] bundle validate Error: bind block references undefined resource "resources.jobs.bar"; define it in the resources section or remove the bind block at targets.default.bind.jobs.bar -Error: bind validation failed +Name: test-bind-orphaned +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bind-orphaned/default + +Found 1 error + +>>> musterr [CLI] bundle plan +Error: bind block references undefined resource "resources.jobs.bar"; define it in the resources section or remove the bind block + at targets.default.bind.jobs.bar diff --git a/acceptance/bundle/deploy/bind/orphaned-bind/script b/acceptance/bundle/deploy/bind/orphaned-bind/script index aeae9c12fa1..5e2f41094d1 100644 --- a/acceptance/bundle/deploy/bind/orphaned-bind/script +++ b/acceptance/bundle/deploy/bind/orphaned-bind/script @@ -1,2 +1,4 @@ -# Import block references jobs.bar but only jobs.foo exists in resources +# Bind block references jobs.bar but only jobs.foo exists in resources. +# This is caught at validation time, before any plan is attempted. +trace musterr $CLI bundle validate trace musterr $CLI bundle plan diff --git a/bundle/config/mutator/validate_bind_resources.go b/bundle/config/mutator/validate_bind_resources.go index 129b90b3e49..ae6d0b29395 100644 --- a/bundle/config/mutator/validate_bind_resources.go +++ b/bundle/config/mutator/validate_bind_resources.go @@ -2,16 +2,20 @@ package mutator import ( "context" + "fmt" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/structs/structpath" ) type validateBindResources struct{} -// ValidateBindResources validates that bind blocks only contain valid resource types. -// Binding is only allowed for resources directly under the resources block, -// not for child resources like permissions or grants. +// ValidateBindResources validates the target's bind blocks against the config: the +// resource type must be bindable and the resource must be defined in the resources +// section. Both are pure config checks, so they run in the initialize phase and +// surface in "bundle validate", before any state is read or plan is attempted. func ValidateBindResources() bundle.Mutator { return &validateBindResources{} } @@ -25,5 +29,37 @@ func (m *validateBindResources) Apply(ctx context.Context, b *bundle.Bundle) dia return nil } - return b.Target.Bind.Validate() + diags := b.Target.Bind.Validate() + if diags.HasError() { + return diags + } + + // A bind block whose resource is not in config has nothing to bind. This also + // covers a resource removed from config while its bind block stayed: the state + // entry would otherwise plan as a delete of the very resource the block protects, + // so the user must drop the block to express that intent. + groups := b.Config.Resources.AllResources() + b.Target.Bind.ForEach(func(resourceType, resourceName, _ string) { + if hasResource(groups, resourceType, resourceName) { + return + } + key := "resources." + resourceType + "." + resourceName + diags = diags.Append(diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("bind block references undefined resource %q; define it in the resources section or remove the bind block", key), + Paths: structpath.NewPathSlice("targets", b.Config.Bundle.Target, "bind", resourceType, resourceName), + }) + }) + + return diags +} + +func hasResource(groups []config.ResourceGroup, resourceType, resourceName string) bool { + for _, group := range groups { + if group.Description.PluralName == resourceType { + _, ok := group.Resources[resourceName] + return ok + } + } + return false } diff --git a/bundle/config/mutator/validate_bind_resources_test.go b/bundle/config/mutator/validate_bind_resources_test.go index b3d5e63d799..e58ebf1e8e4 100644 --- a/bundle/config/mutator/validate_bind_resources_test.go +++ b/bundle/config/mutator/validate_bind_resources_test.go @@ -5,6 +5,7 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/resources" "github.com/stretchr/testify/assert" ) @@ -14,8 +15,13 @@ func TestValidateBindResourcesNoTarget(t *testing.T) { assert.NoError(t, diags.Error()) } -func TestValidateBindResourcesAcceptsTopLevel(t *testing.T) { +func TestValidateBindResourcesAcceptsDefinedResource(t *testing.T) { b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Jobs: map[string]*resources.Job{"foo": {}}, + }, + }, Target: &config.Target{ Bind: config.Bind{ "jobs": {"foo": {ID: "1"}}, @@ -38,3 +44,23 @@ func TestValidateBindResourcesRejectsUnsupportedType(t *testing.T) { assert.Error(t, diags.Error()) assert.Contains(t, diags[0].Summary, `binding "jobs.permissions" is not supported`) } + +func TestValidateBindResourcesRejectsUndefinedResource(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Bundle: config.Bundle{Target: "dev"}, + Resources: config.Resources{ + Jobs: map[string]*resources.Job{"foo": {}}, + }, + }, + Target: &config.Target{ + Bind: config.Bind{ + "jobs": {"bar": {ID: "1"}}, + }, + }, + } + diags := bundle.Apply(t.Context(), b, ValidateBindResources()) + assert.Error(t, diags.Error()) + assert.Equal(t, `bind block references undefined resource "resources.jobs.bar"; define it in the resources section or remove the bind block`, diags[0].Summary) + assert.Equal(t, "targets.dev.bind.jobs.bar", diags[0].Paths[0].String()) +} diff --git a/bundle/direct/bundle_plan_bind.go b/bundle/direct/bundle_plan_bind.go index bde6f3931bd..19095989b48 100644 --- a/bundle/direct/bundle_plan_bind.go +++ b/bundle/direct/bundle_plan_bind.go @@ -30,21 +30,8 @@ func (b *DeploymentBundle) validateBindConfig(ctx context.Context, configRoot *c hasError := false targetName := configRoot.Bundle.Target - // Bind blocks must reference resources that exist in the merged config. - bindConfig.ForEach(func(resourceType, resourceName, bindID string) { - key := "resources." + resourceType + "." + resourceName - if _, ok := b.Plan.Plan[key]; ok { - return - } - bindPath := bindBlockPath(targetName, resourceType, resourceName) - logdiag.LogDiag(ctx, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("bind block references undefined resource %q; define it in the resources section or remove the bind block", key), - Locations: configRoot.GetLocationsOf(bindPath), - Paths: []*structpath.PathNode{bindPath}, - }) - hasError = true - }) + // Bind blocks referencing resources missing from config are rejected by + // mutator.ValidateBindResources before planning; the checks below need state. // A resource that is already in state cannot be re-bound to a different ID: the // bind block is meant to import a resource for the first time. When the IDs match From 522860819d06a7a487fd90d6522574ff58605e9b Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:09:48 +0000 Subject: [PATCH 03/20] Error instead of recreating a bound resource that was deleted remotely When a bound resource (in state, bind block present) no longer exists in the workspace, the in-state plan path mapped the 404 to a create. The deploy then created a replacement under a new ID, and every following plan failed with 'already bound to a different ID' until the user edited the bind block. A bind block pins the resource to one ID, so report the deletion and let the user decide: remove the block to create a new resource. Co-authored-by: Isaac --- .../deploy/bind/gone-in-state/databricks.yml | 23 +++++++++++++++ .../deploy/bind/gone-in-state/out.test.toml | 2 ++ .../deploy/bind/gone-in-state/output.txt | 28 +++++++++++++++++++ .../bundle/deploy/bind/gone-in-state/script | 14 ++++++++++ bundle/direct/bundle_plan.go | 10 ++++++- 5 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 acceptance/bundle/deploy/bind/gone-in-state/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/gone-in-state/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/gone-in-state/output.txt create mode 100644 acceptance/bundle/deploy/bind/gone-in-state/script diff --git a/acceptance/bundle/deploy/bind/gone-in-state/databricks.yml b/acceptance/bundle/deploy/bind/gone-in-state/databricks.yml new file mode 100644 index 00000000000..307c3fad567 --- /dev/null +++ b/acceptance/bundle/deploy/bind/gone-in-state/databricks.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-gone-in-state + +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: /Workspace/test.py + +targets: + default: + bind: + jobs: + foo: + id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/gone-in-state/out.test.toml b/acceptance/bundle/deploy/bind/gone-in-state/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/gone-in-state/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/gone-in-state/output.txt b/acceptance/bundle/deploy/bind/gone-in-state/output.txt new file mode 100644 index 00000000000..df199fd1a71 --- /dev/null +++ b/acceptance/bundle/deploy/bind/gone-in-state/output.txt @@ -0,0 +1,28 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-gone-in-state/default/files... +Bound and updated jobs.foo +Files: 1 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound + +=== Delete the bound job out-of-band: the bind block pins that ID, so the deploy must not create a replacement +>>> [CLI] jobs delete [NEW_JOB_ID] + +>>> musterr [CLI] bundle plan +Error: cannot plan resources.jobs.foo: bound resource with ID "[NEW_JOB_ID]" no longer exists in the workspace; remove the bind block to create a new resource + +Error: planning failed + + +>>> musterr [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-gone-in-state/default/files... +Error: cannot plan resources.jobs.foo: bound resource with ID "[NEW_JOB_ID]" no longer exists in the workspace; remove the bind block to create a new resource + +Error: planning failed + +Files: 0 uploaded, 0 deleted + +>>> [CLI] bundle destroy --auto-approve +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-gone-in-state/default + +Destroy: 0 deleted diff --git a/acceptance/bundle/deploy/bind/gone-in-state/script b/acceptance/bundle/deploy/bind/gone-in-state/script new file mode 100644 index 00000000000..db0c9fdcfc9 --- /dev/null +++ b/acceptance/bundle/deploy/bind/gone-in-state/script @@ -0,0 +1,14 @@ +# Create a job in the workspace and bind it +NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" + +trace $CLI bundle deploy --auto-approve + +title "Delete the bound job out-of-band: the bind block pins that ID, so the deploy must not create a replacement" +trace $CLI jobs delete $NEW_JOB_ID +trace musterr $CLI bundle plan +trace musterr $CLI bundle deploy --auto-approve + +# Cleanup: the job is already gone, destroy only drops the state entry +trace $CLI bundle destroy --auto-approve diff --git a/bundle/direct/bundle_plan.go b/bundle/direct/bundle_plan.go index 19c9a2420c4..8c001fb6b36 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -395,7 +395,15 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks return false } + _, bound := bindIDByKey[resourceKey] if remoteState == nil { + // A bind block pins the resource to one ID. Creating a replacement would + // re-point the state at a new ID behind the user's back and leave the + // block mismatched on every later plan, so report the deletion instead. + if bound { + logdiag.LogError(ctx, fmt.Errorf("%s: bound resource with ID %q no longer exists in the workspace; remove the bind block to create a new resource", errorPrefix, dbentry.ID)) + return false + } // Even if local action is "recreate" which is higher than "create", we should still pick "create" here // because we know remote does not exist. action = deployplan.Create @@ -411,7 +419,7 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks // it is defined, not just during the first bind. Once the resource is in // state, a config change that would recreate or re-ID it still surfaces as an // error so the bind block behaves as a persistent guard (see handleBindPlan). - if _, bound := bindIDByKey[resourceKey]; bound && (action == deployplan.Recreate || action == deployplan.UpdateWithID) { + if bound && (action == deployplan.Recreate || action == deployplan.UpdateWithID) { logdiag.LogError(ctx, buildBindConflictError("cannot bind "+resourceKey, action, entry.Changes, resourceKey)) return false } From 57f8a75775033925082a020492d7ddeaa5d06d52 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:20:36 +0000 Subject: [PATCH 04/20] Reject a root-level bind block instead of ignoring it A bind block outside targets was dropped as an unknown field with only a warning, and the deploy then created the resources it named instead of binding the existing ones. Fail config loading with a pointer to targets..bind instead. Co-authored-by: Isaac --- .../deploy/bind/top-level-bind/output.txt | 20 ++++++++++--------- .../bundle/deploy/bind/top-level-bind/script | 8 +++++--- bundle/config/root.go | 12 +++++++++++ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/acceptance/bundle/deploy/bind/top-level-bind/output.txt b/acceptance/bundle/deploy/bind/top-level-bind/output.txt index ee8fd54ee74..3a7e5f1c9bc 100644 --- a/acceptance/bundle/deploy/bind/top-level-bind/output.txt +++ b/acceptance/bundle/deploy/bind/top-level-bind/output.txt @@ -1,12 +1,14 @@ ->>> [CLI] bundle validate -Warning: unknown field: bind - in databricks.yml:4:1 +>>> musterr [CLI] bundle validate +Error: bind blocks are only supported under targets..bind, not at the root of the configuration + at bind + in databricks.yml:5:3 -Name: test-bind-top-level -Target: default -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/test-bind-top-level/default -Found 1 warning +Found 1 error + +>>> musterr [CLI] bundle plan +Error: bind blocks are only supported under targets..bind, not at the root of the configuration + at bind + in databricks.yml:5:3 + diff --git a/acceptance/bundle/deploy/bind/top-level-bind/script b/acceptance/bundle/deploy/bind/top-level-bind/script index fd58a501146..43a0bfe2e1e 100644 --- a/acceptance/bundle/deploy/bind/top-level-bind/script +++ b/acceptance/bundle/deploy/bind/top-level-bind/script @@ -1,3 +1,5 @@ -# A bind block at the root level (outside a target) is not recognized as a -# config field, so it surfaces as an "unknown field" warning and is ignored. -trace $CLI bundle validate +# A bind block at the root level (outside a target) is a misplacement. It must be +# rejected: ignoring it as an unknown field would create the resource instead of +# binding the existing one. +trace musterr $CLI bundle validate +trace musterr $CLI bundle plan diff --git a/bundle/config/root.go b/bundle/config/root.go index a64b742630e..76293d715c8 100644 --- a/bundle/config/root.go +++ b/bundle/config/root.go @@ -131,6 +131,18 @@ func LoadFromBytes(path string, raw []byte) (*Root, diag.Diagnostics) { return nil, diag.Errorf("failed to rewrite %s: %v", path, err) } + // A root-level bind block is a misplaced target-level block. Normalize would drop + // it as an unknown field with only a warning, and the deploy would then create the + // resources it names instead of binding the existing ones. + if bind, err := dyn.GetByPath(v, dyn.NewPath(dyn.Key("bind"))); err == nil { + return nil, diag.Diagnostics{{ + Severity: diag.Error, + Summary: "bind blocks are only supported under targets..bind, not at the root of the configuration", + Locations: bind.Locations(), + Paths: structpath.NewPathSlice("bind"), + }} + } + // Normalize dynamic configuration tree according to configuration type. v, diags := convert.Normalize(r, v) From 7b743f23a9e374beddb6816b06cfd59262323c5c Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:21:33 +0000 Subject: [PATCH 05/20] Resolve variable references in bind IDs SelectTarget keeps the selected target as a typed snapshot and drops the targets section from the config tree, so ${var.*} in a bind ID was never interpolated: the plan tried to read a job with the literal ID '${var.job_id}'. The generated schema already advertises the variable pattern for this field. Add a mutator that resolves bundle, workspace and variable references in the target's bind IDs after the regular variable resolution, sharing the ${var.x} shorthand rewrite with it. Co-authored-by: Isaac --- .../deploy/bind/variable-id/databricks.yml | 28 +++++++ .../deploy/bind/variable-id/out.test.toml | 2 + .../bundle/deploy/bind/variable-id/output.txt | 22 ++++++ .../bundle/deploy/bind/variable-id/script | 11 +++ .../config/mutator/resolve_bind_references.go | 65 +++++++++++++++++ .../mutator/resolve_bind_references_test.go | 73 +++++++++++++++++++ .../mutator/resolve_variable_references.go | 43 ++++++----- bundle/phases/initialize.go | 8 +- 8 files changed, 230 insertions(+), 22 deletions(-) create mode 100644 acceptance/bundle/deploy/bind/variable-id/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/variable-id/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/variable-id/output.txt create mode 100644 acceptance/bundle/deploy/bind/variable-id/script create mode 100644 bundle/config/mutator/resolve_bind_references.go create mode 100644 bundle/config/mutator/resolve_bind_references_test.go diff --git a/acceptance/bundle/deploy/bind/variable-id/databricks.yml b/acceptance/bundle/deploy/bind/variable-id/databricks.yml new file mode 100644 index 00000000000..50002aaa239 --- /dev/null +++ b/acceptance/bundle/deploy/bind/variable-id/databricks.yml @@ -0,0 +1,28 @@ +bundle: + name: test-bind-variable-id + +variables: + job_id: + description: ID of the existing job to bind + default: "PLACEHOLDER_JOB_ID" + +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: /Workspace/test.py + +targets: + default: + bind: + jobs: + foo: + id: ${var.job_id} diff --git a/acceptance/bundle/deploy/bind/variable-id/out.test.toml b/acceptance/bundle/deploy/bind/variable-id/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/variable-id/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/variable-id/output.txt b/acceptance/bundle/deploy/bind/variable-id/output.txt new file mode 100644 index 00000000000..89f80ee5f95 --- /dev/null +++ b/acceptance/bundle/deploy/bind/variable-id/output.txt @@ -0,0 +1,22 @@ + +>>> [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-variable-id/default/files... +Bound and updated jobs.foo +Files: 1 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound + +>>> print_state.py +json.state.resources.jobs.foo.__id__ = "[NEW_JOB_ID]"; + +>>> [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-variable-id/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/variable-id/script b/acceptance/bundle/deploy/bind/variable-id/script new file mode 100644 index 00000000000..2a1f388bd63 --- /dev/null +++ b/acceptance/bundle/deploy/bind/variable-id/script @@ -0,0 +1,11 @@ +# Create a job in the workspace; its ID reaches the bind block through a variable +NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" + +trace $CLI bundle plan +trace $CLI bundle deploy --auto-approve +trace print_state.py | gron.py | grep __id__ + +# Cleanup +trace $CLI bundle destroy --auto-approve diff --git a/bundle/config/mutator/resolve_bind_references.go b/bundle/config/mutator/resolve_bind_references.go new file mode 100644 index 00000000000..b6b096c0e26 --- /dev/null +++ b/bundle/config/mutator/resolve_bind_references.go @@ -0,0 +1,65 @@ +package mutator + +import ( + "context" + "slices" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/cli/libs/dyn/convert" + "github.com/databricks/cli/libs/dyn/dynvar" +) + +type resolveBindReferences struct{} + +// ResolveBindReferences resolves ${var.*}, ${bundle.*} and ${workspace.*} references +// in the selected target's bind IDs. SelectTarget keeps the target as a typed snapshot +// and drops the targets section from the config tree, so the regular variable +// resolution never sees bind blocks. +func ResolveBindReferences() bundle.Mutator { + return &resolveBindReferences{} +} + +func (m *resolveBindReferences) Name() string { + return "ResolveBindReferences" +} + +func (m *resolveBindReferences) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { + if b.Target == nil || b.Target.Bind.IsEmpty() { + return nil + } + + bind, err := convert.FromTyped(b.Target.Bind, dyn.NilValue) + if err != nil { + return diag.FromErr(err) + } + + prefixes := make([]dyn.Path, len(defaultPrefixes)) + for i, prefix := range defaultPrefixes { + prefixes[i] = dyn.MustPathFromString(prefix) + } + + // Same lookup root as resolveVariableReferences: fields present in the type but + // unset in config resolve to their empty value instead of failing. + normalized, _ := convert.Normalize(b.Config, b.Config.Value(), convert.IncludeMissingFields) + resolved, err := dynvar.Resolve(bind, func(path dyn.Path) (dyn.Value, error) { + path = rewriteVarShorthand(path) + if !slices.ContainsFunc(prefixes, path.HasPrefix) { + return dyn.InvalidValue, dynvar.ErrSkipResolution + } + return lookup(normalized, path, b) + }) + if err != nil { + return diag.FromErr(err) + } + + // A reference may resolve to a non-string (e.g. an integer job ID), so normalize + // against the field types before converting back. + resolved, diags := convert.Normalize(b.Target.Bind, resolved) + if diags.HasError() { + return diags + } + + return diags.Extend(diag.FromErr(convert.ToTyped(&b.Target.Bind, resolved))) +} diff --git a/bundle/config/mutator/resolve_bind_references_test.go b/bundle/config/mutator/resolve_bind_references_test.go new file mode 100644 index 00000000000..dbb0b7190df --- /dev/null +++ b/bundle/config/mutator/resolve_bind_references_test.go @@ -0,0 +1,73 @@ +package mutator + +import ( + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/variable" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolveBindReferences(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Bundle: config.Bundle{Target: "dev"}, + Variables: map[string]*variable.Variable{ + "job_id": {Value: "123"}, + }, + }, + Target: &config.Target{ + Bind: config.Bind{ + "jobs": { + "foo": {ID: "${var.job_id}"}, + "bar": {ID: "${bundle.target}-456"}, + }, + }, + }, + } + + diags := bundle.Apply(t.Context(), b, ResolveBindReferences()) + require.NoError(t, diags.Error()) + assert.Equal(t, "123", b.Target.Bind["jobs"]["foo"].ID) + assert.Equal(t, "dev-456", b.Target.Bind["jobs"]["bar"].ID) +} + +func TestResolveBindReferencesIntegerValue(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Variables: map[string]*variable.Variable{ + "job_id": {Value: 123}, + }, + }, + Target: &config.Target{ + Bind: config.Bind{ + "jobs": {"foo": {ID: "${var.job_id}"}}, + }, + }, + } + + diags := bundle.Apply(t.Context(), b, ResolveBindReferences()) + require.NoError(t, diags.Error()) + assert.Equal(t, "123", b.Target.Bind["jobs"]["foo"].ID) +} + +func TestResolveBindReferencesUnknownVariable(t *testing.T) { + b := &bundle.Bundle{ + Target: &config.Target{ + Bind: config.Bind{ + "jobs": {"foo": {ID: "${var.missing}"}}, + }, + }, + } + + diags := bundle.Apply(t.Context(), b, ResolveBindReferences()) + assert.Error(t, diags.Error()) +} + +func TestResolveBindReferencesNoTarget(t *testing.T) { + b := &bundle.Bundle{} + diags := bundle.Apply(t.Context(), b, ResolveBindReferences()) + assert.NoError(t, diags.Error()) +} diff --git a/bundle/config/mutator/resolve_variable_references.go b/bundle/config/mutator/resolve_variable_references.go index 42c22b61533..cd9ba820cc8 100644 --- a/bundle/config/mutator/resolve_variable_references.go +++ b/bundle/config/mutator/resolve_variable_references.go @@ -164,15 +164,11 @@ func (m *resolveVariableReferences) Apply(ctx context.Context, b *bundle.Bundle) prefixes[i] = dyn.MustPathFromString(prefix) } - // The path ${var.foo} is a shorthand for ${variables.foo.value}. - // We rewrite it here to make the resolution logic simpler. - varPath := dyn.NewPath(dyn.Key("var")) - var diags diag.Diagnostics maxRounds := 1 + m.extraRounds for round := range maxRounds { - hasUpdates, newDiags := m.resolveOnce(b, prefixes, varPath) + hasUpdates, newDiags := m.resolveOnce(b, prefixes) diags = diags.Extend(newDiags) @@ -201,7 +197,27 @@ func (m *resolveVariableReferences) Apply(ctx context.Context, b *bundle.Bundle) return diags } -func (m *resolveVariableReferences) resolveOnce(b *bundle.Bundle, prefixes []dyn.Path, varPath dyn.Path) (bool, diag.Diagnostics) { +// varPath is the ${var.foo} shorthand prefix for ${variables.foo.value}. +var varPath = dyn.NewPath(dyn.Key("var")) + +// rewriteVarShorthand rewrites the shorthand path ${var.foo} into ${variables.foo.value}, +// so that resolution only has to deal with the long form. +func rewriteVarShorthand(path dyn.Path) dyn.Path { + if !path.HasPrefix(varPath) { + return path + } + newPath := dyn.NewPath( + dyn.Key("variables"), + path[1], + dyn.Key("value"), + ) + if len(path) > 2 { + newPath = newPath.Append(path[2:]...) + } + return newPath +} + +func (m *resolveVariableReferences) resolveOnce(b *bundle.Bundle, prefixes []dyn.Path) (bool, diag.Diagnostics) { var diags diag.Diagnostics hasUpdates := false err := m.selectivelyMutate(b, func(root dyn.Value) (dyn.Value, error) { @@ -224,20 +240,7 @@ func (m *resolveVariableReferences) resolveOnce(b *bundle.Bundle, prefixes []dyn root, err := dyn.MapByPattern(root, m.pattern, func(p dyn.Path, v dyn.Value) (dyn.Value, error) { // Resolve variable references in all values. return dynvar.Resolve(v, func(path dyn.Path) (dyn.Value, error) { - // Rewrite the shorthand path ${var.foo} into ${variables.foo.value}. - if path.HasPrefix(varPath) { - newPath := dyn.NewPath( - dyn.Key("variables"), - path[1], - dyn.Key("value"), - ) - - if len(path) > 2 { - newPath = newPath.Append(path[2:]...) - } - - path = newPath - } + path = rewriteVarShorthand(path) // If the path starts with "artifacts", we need to add a metric to track if this reference is used. if path.HasPrefix(artifactPath) { diff --git a/bundle/phases/initialize.go b/bundle/phases/initialize.go index 4fe3dc86bfb..3e8e0d56de9 100644 --- a/bundle/phases/initialize.go +++ b/bundle/phases/initialize.go @@ -255,8 +255,12 @@ func Initialize(ctx context.Context, b *bundle.Bundle) { // Validates that when using a shared workspace path, appropriate permissions are configured permissions.ValidateSharedRootPermissions(), - // Reads (typed): b.Config.Targets..Bind (checks for permissions/grants under bind, which are not bindable) - // Reports validation errors when bind blocks reference child resources instead of top-level resources. + // Reads (dynamic): bundle, workspace, variables + // Updates (typed): b.Target.Bind[*][*].ID (resolves ${var.*} and similar references in bind IDs) + mutator.ResolveBindReferences(), + + // Reads (typed): b.Target.Bind, b.Config.Resources + // Reports validation errors when a bind block names an unsupported resource type or a resource that is not defined. mutator.ValidateBindResources(), // Annotate resources with "deployment" metadata. From 9c8363649e64f08ddce2b92f0caeefaec5cfa0f1 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:22:00 +0000 Subject: [PATCH 06/20] Tighten bind planning guards and use the bind ID known at plan time - Scope the state ID collision check to the bind's own resource type: IDs are only unique per type, so a job and an experiment may share one. - Resolve ${resources..id} references to a resource being bound from the bind block instead of deferring them to after apply; the ID is known. - Reuse the remote state read at plan time in Bind, so dependents such as permissions no longer trigger a second read of the bound resource. - Reword the persistent recreate guard for resources already in state: they are bound, not being bound, and 'make this a bind-only operation' does not apply. Drop the trailing newline from the error text. - Mirror the in-state DoUpdate check in first-time bind planning so a resource without DoUpdate fails at plan time, not at apply time. - Return a status from handleBindPlan instead of reading the shared error flag, which is racy under parallel planning. Co-authored-by: Isaac --- .../databricks.yml | 19 +++++ .../databricks_with_bind.yml | 26 +++++++ .../id-collision-different-type/out.test.toml | 2 + .../id-collision-different-type/output.txt | 20 +++++ .../bind/id-collision-different-type/script | 16 ++++ .../id-collision-different-type/test.toml | 1 + .../bind/recreate-blocked-in-state/output.txt | 7 +- .../deploy/bind/recreate-blocked/output.txt | 1 - .../deploy/bind/ref-to-bound/databricks.yml | 29 +++++++ .../deploy/bind/ref-to-bound/out.test.toml | 2 + .../deploy/bind/ref-to-bound/output.txt | 29 +++++++ .../bundle/deploy/bind/ref-to-bound/script | 16 ++++ bundle/direct/apply.go | 13 +++- bundle/direct/bundle_plan.go | 10 ++- bundle/direct/bundle_plan_bind.go | 76 +++++++++++++------ 15 files changed, 234 insertions(+), 33 deletions(-) create mode 100644 acceptance/bundle/deploy/bind/id-collision-different-type/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/id-collision-different-type/databricks_with_bind.yml create mode 100644 acceptance/bundle/deploy/bind/id-collision-different-type/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/id-collision-different-type/output.txt create mode 100644 acceptance/bundle/deploy/bind/id-collision-different-type/script create mode 100644 acceptance/bundle/deploy/bind/id-collision-different-type/test.toml create mode 100644 acceptance/bundle/deploy/bind/ref-to-bound/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/ref-to-bound/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/ref-to-bound/output.txt create mode 100644 acceptance/bundle/deploy/bind/ref-to-bound/script diff --git a/acceptance/bundle/deploy/bind/id-collision-different-type/databricks.yml b/acceptance/bundle/deploy/bind/id-collision-different-type/databricks.yml new file mode 100644 index 00000000000..d0c26c5697b --- /dev/null +++ b/acceptance/bundle/deploy/bind/id-collision-different-type/databricks.yml @@ -0,0 +1,19 @@ +bundle: + name: test-bind-id-collision-type + +resources: + jobs: + foo: + name: test-bind-collision-job + environments: + - environment_key: default + spec: + client: "1" + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: /Workspace/test.py + +targets: + default: diff --git a/acceptance/bundle/deploy/bind/id-collision-different-type/databricks_with_bind.yml b/acceptance/bundle/deploy/bind/id-collision-different-type/databricks_with_bind.yml new file mode 100644 index 00000000000..88fca7da067 --- /dev/null +++ b/acceptance/bundle/deploy/bind/id-collision-different-type/databricks_with_bind.yml @@ -0,0 +1,26 @@ +bundle: + name: test-bind-id-collision-type + +resources: + jobs: + foo: + name: test-bind-collision-job + environments: + - environment_key: default + spec: + client: "1" + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: /Workspace/test.py + experiments: + exp: + name: /Shared/test-bind-collision-exp + +targets: + default: + bind: + experiments: + exp: + id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/id-collision-different-type/out.test.toml b/acceptance/bundle/deploy/bind/id-collision-different-type/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/id-collision-different-type/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/id-collision-different-type/output.txt b/acceptance/bundle/deploy/bind/id-collision-different-type/output.txt new file mode 100644 index 00000000000..f425e23564d --- /dev/null +++ b/acceptance/bundle/deploy/bind/id-collision-different-type/output.txt @@ -0,0 +1,20 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-id-collision-type/default/files... +Created jobs.foo +Files: 2 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +>>> musterr [CLI] bundle plan +Error: cannot bind resources.experiments.exp: resource with ID "[FOO_ID]" does not exist in workspace + +Error: planning failed + + +>>> [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-id-collision-type/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/id-collision-different-type/script b/acceptance/bundle/deploy/bind/id-collision-different-type/script new file mode 100644 index 00000000000..6c4bb5411fe --- /dev/null +++ b/acceptance/bundle/deploy/bind/id-collision-different-type/script @@ -0,0 +1,16 @@ +# Deploy foo to create it in state +trace $CLI bundle deploy --auto-approve + +JOB_ID=$(read_id.py foo) + +# Bind an experiment whose ID happens to equal the job's ID. IDs are only unique +# per resource type, so this is not a collision with jobs.foo; the bind proceeds and +# fails only because no experiment with that ID exists. +cp databricks.yml databricks.yml.bak +cp databricks_with_bind.yml databricks.yml +update_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$JOB_ID" +trace musterr $CLI bundle plan + +# Cleanup +cp databricks.yml.bak databricks.yml +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deploy/bind/id-collision-different-type/test.toml b/acceptance/bundle/deploy/bind/id-collision-different-type/test.toml new file mode 100644 index 00000000000..f70b5c78fff --- /dev/null +++ b/acceptance/bundle/deploy/bind/id-collision-different-type/test.toml @@ -0,0 +1 @@ +Ignore = [".databricks", "databricks.yml.bak", "databricks_with_bind.yml"] diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt index 8fd7ce49d3f..3100e00ee3f 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt @@ -6,16 +6,16 @@ Files: 2 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound >>> musterr [CLI] bundle plan -Error: cannot bind resources.pipelines.foo: cannot recreate resource with bind block +Error: cannot plan resources.pipelines.foo: bind block prevents recreating the bound resource -This would destroy and recreate the existing workspace resource, changing its ID. +This would destroy and recreate the bound workspace resource, changing its ID. The following fields cannot be modified because they require resource recreation: - storage (immutable) To resolve this issue, you have two options: -1. Remove the problematic fields from your configuration to make this a bind-only operation: +1. Revert the problematic fields in your configuration to keep the bound resource as it is: resources: pipelines: @@ -33,6 +33,5 @@ To resolve this issue, you have two options: # foo: # id: - Error: planning failed diff --git a/acceptance/bundle/deploy/bind/recreate-blocked/output.txt b/acceptance/bundle/deploy/bind/recreate-blocked/output.txt index 168068d9ca0..ecf96ca9021 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked/output.txt +++ b/acceptance/bundle/deploy/bind/recreate-blocked/output.txt @@ -27,6 +27,5 @@ To resolve this issue, you have two options: # foo: # id: - Error: planning failed diff --git a/acceptance/bundle/deploy/bind/ref-to-bound/databricks.yml b/acceptance/bundle/deploy/bind/ref-to-bound/databricks.yml new file mode 100644 index 00000000000..9bda212160e --- /dev/null +++ b/acceptance/bundle/deploy/bind/ref-to-bound/databricks.yml @@ -0,0 +1,29 @@ +bundle: + name: test-bind-ref-to-bound + +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: /Workspace/test.py + bar: + name: test-bind-runner + tasks: + - task_key: run_foo + run_job_task: + job_id: ${resources.jobs.foo.id} + +targets: + default: + bind: + jobs: + foo: + id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/ref-to-bound/out.test.toml b/acceptance/bundle/deploy/bind/ref-to-bound/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/ref-to-bound/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/ref-to-bound/output.txt b/acceptance/bundle/deploy/bind/ref-to-bound/output.txt new file mode 100644 index 00000000000..ed7be1ceb39 --- /dev/null +++ b/acceptance/bundle/deploy/bind/ref-to-bound/output.txt @@ -0,0 +1,29 @@ + +>>> [CLI] bundle plan +create jobs.bar +bind_and_update jobs.foo (id: [NEW_JOB_ID]) + +Plan: 1 to add, 1 to change, 0 to delete, 0 unchanged, 1 to bind + +=== The bound ID is known at plan time, so the reference is already resolved in the plan +>>> [CLI] bundle plan -o json +json.plan.resources.jobs.bar.new_state.value.tasks[0].run_job_task.job_id = [NEW_JOB_ID]; + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-ref-to-bound/default/files... +Bound and updated jobs.foo +Created jobs.bar +Files: 1 uploaded, 0 deleted +Resources: 1 created, 1 changed, 0 deleted, 0 unchanged, 1 bound + +>>> [CLI] jobs get [BAR_ID] +[NEW_JOB_ID] + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.jobs.bar + delete resources.jobs.foo + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-ref-to-bound/default + +Destroy: 2 deleted diff --git a/acceptance/bundle/deploy/bind/ref-to-bound/script b/acceptance/bundle/deploy/bind/ref-to-bound/script new file mode 100644 index 00000000000..56a4d70d45e --- /dev/null +++ b/acceptance/bundle/deploy/bind/ref-to-bound/script @@ -0,0 +1,16 @@ +# Create a job in the workspace; bar refers to it through ${resources.jobs.foo.id} +NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" + +trace $CLI bundle plan + +title "The bound ID is known at plan time, so the reference is already resolved in the plan" +trace $CLI bundle plan -o json | gron.py | grep run_job_task + +trace $CLI bundle deploy --auto-approve +BAR_ID=$(read_id.py bar) +trace $CLI jobs get $BAR_ID | jq '.settings.tasks[0].run_job_task.job_id' + +# Cleanup +trace $CLI bundle destroy --auto-approve diff --git a/bundle/direct/apply.go b/bundle/direct/apply.go index 04486d879d0..a9fe413fb68 100644 --- a/bundle/direct/apply.go +++ b/bundle/direct/apply.go @@ -40,7 +40,7 @@ func (d *DeploymentUnit) Deploy(ctx context.Context, db *dstate.DeploymentState, case deployplan.Create: return d.Create(ctx, db, newState) case deployplan.Bind: - return d.Bind(ctx, db, planEntry.BindID, newState) + return d.Bind(ctx, db, planEntry.BindID, newState, planEntry) case deployplan.BindAndUpdate: return d.BindAndUpdate(ctx, db, planEntry.BindID, newState, planEntry) } @@ -66,8 +66,15 @@ func (d *DeploymentUnit) Deploy(ctx context.Context, db *dstate.DeploymentState, // Bind brings an existing workspace resource under bundle management by persisting // its ID to state, without modifying the remote resource. -func (d *DeploymentUnit) Bind(ctx context.Context, db *dstate.DeploymentState, id string, newState any) error { - err := db.SaveState(ctx, d.ResourceKey, id, newState, d.DependsOn) +func (d *DeploymentUnit) Bind(ctx context.Context, db *dstate.DeploymentState, id string, newState any, planEntry *deployplan.PlanEntry) error { + // The remote resource was read during planning and is unchanged by a pure bind; + // reuse it so dependents (permissions, grants) do not trigger a second read. + err := d.SetRemoteState(planEntry.RemoteState) + if err != nil { + return err + } + + err = db.SaveState(ctx, d.ResourceKey, id, newState, d.DependsOn) if err != nil { return fmt.Errorf("saving state id=%s: %w", id, err) } diff --git a/bundle/direct/bundle_plan.go b/bundle/direct/bundle_plan.go index 8c001fb6b36..cf5d39db8a0 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -297,8 +297,7 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks // plan a Bind / BindAndUpdate instead of a Create. if bindID, ok := bindIDByKey[resourceKey]; ok { entry.BindID = bindID - b.handleBindPlan(ctx, resourceKey, entry, adapter) - return !logdiag.HasError(ctx) + return b.handleBindPlan(ctx, resourceKey, entry, adapter) } entry.Action = deployplan.Create return true @@ -420,7 +419,7 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks // state, a config change that would recreate or re-ID it still surfaces as an // error so the bind block behaves as a persistent guard (see handleBindPlan). if bound && (action == deployplan.Recreate || action == deployplan.UpdateWithID) { - logdiag.LogError(ctx, buildBindConflictError("cannot bind "+resourceKey, action, entry.Changes, resourceKey)) + logdiag.LogError(ctx, buildBindConflictError(resourceKey, action, entry.Changes, false)) return false } @@ -1033,6 +1032,11 @@ func (b *DeploymentBundle) LookupReferencePreDeploy(ctx context.Context, path *s } return id, nil } + // A bind adopts the resource under the ID from the bind block; it is known + // before deployment even though the state has no entry yet. + if targetEntry.BindID != "" { + return targetEntry.BindID, nil + } // id may change after deployment, this needs to be done later return nil, errDelayed } diff --git a/bundle/direct/bundle_plan_bind.go b/bundle/direct/bundle_plan_bind.go index 19095989b48..fe671472609 100644 --- a/bundle/direct/bundle_plan_bind.go +++ b/bundle/direct/bundle_plan_bind.go @@ -80,6 +80,8 @@ func (b *DeploymentBundle) validateBindConfig(ctx context.Context, configRoot *c // A bind ID must not collide with another resource's ID in state. Such a collision // would let a bind silently redirect a delete/recreate/update_id at a resource the // user is also trying to import; reject it instead of choosing one over the other. + // IDs are only unique within a resource type (a job and an experiment can share a + // numeric ID), so only entries of the bind's own type can collide. stateKeys := make([]string, 0, len(b.StateDB.Data.State)) for stateKey := range b.StateDB.Data.State { stateKeys = append(stateKeys, stateKey) @@ -93,6 +95,9 @@ func (b *DeploymentBundle) validateBindConfig(ctx context.Context, configRoot *c if stateKey == bindKey || stateEntry.ID != bindID { continue } + if stateType, _ := splitResourceKey(stateKey); stateType != resourceType { + continue + } bindPath := bindBlockPath(targetName, resourceType, resourceName) logdiag.LogDiag(ctx, diag.Diagnostic{ Severity: diag.Error, @@ -113,7 +118,8 @@ func (b *DeploymentBundle) validateBindConfig(ctx context.Context, configRoot *c // // Recreate / UpdateWithID actions imply destroying or replacing the workspace // resource — incompatible with bind — so they surface as actionable errors. -func (b *DeploymentBundle) handleBindPlan(ctx context.Context, resourceKey string, entry *deployplan.PlanEntry, adapter *dresources.Adapter) { +// Returns false if an error was logged. +func (b *DeploymentBundle) handleBindPlan(ctx context.Context, resourceKey string, entry *deployplan.PlanEntry, adapter *dresources.Adapter) bool { bindID := entry.BindID errorPrefix := "cannot bind " + resourceKey @@ -126,7 +132,7 @@ func (b *DeploymentBundle) handleBindPlan(ctx context.Context, resourceKey strin } else { logdiag.LogError(ctx, fmt.Errorf("%s: reading remote resource id=%q: %w", errorPrefix, bindID, err)) } - return + return false } entry.RemoteState = remoteState @@ -135,13 +141,13 @@ func (b *DeploymentBundle) handleBindPlan(ctx context.Context, resourceKey strin sv, ok := b.StateCache.Load(resourceKey) if !ok { logdiag.LogError(ctx, fmt.Errorf("%s: internal error: no state cache entry", errorPrefix)) - return + return false } remoteStateComparable, err := adapter.RemapState(remoteState) if err != nil { logdiag.LogError(ctx, fmt.Errorf("%s: interpreting remote state: %w", errorPrefix, err)) - return + return false } // Compact both sides on the same fields, so a hashed_fields field is a hash on @@ -149,49 +155,59 @@ func (b *DeploymentBundle) handleBindPlan(ctx context.Context, resourceKey strin remoteStateComparable, err = dresources.CompactState(adapter.ResourceConfig(), remoteStateComparable) if err != nil { logdiag.LogError(ctx, fmt.Errorf("%s: compacting remote state: %w", errorPrefix, err)) - return + return false } localState, err := dresources.CompactState(adapter.ResourceConfig(), sv.Value) if err != nil { logdiag.LogError(ctx, fmt.Errorf("%s: compacting local state: %w", errorPrefix, err)) - return + return false } remoteDiff, err := structdiff.GetStructDiff(remoteStateComparable, localState, adapter.KeyedSlices()) if err != nil { logdiag.LogError(ctx, fmt.Errorf("%s: diffing remote state: %w", errorPrefix, err)) - return + return false } // No "saved state" exists for a first-time bind; the diff is purely remote vs. config. entry.Changes, err = prepareChanges(ctx, adapter, nil, remoteDiff, nil, remoteStateComparable) if err != nil { logdiag.LogError(ctx, fmt.Errorf("%s: %w", errorPrefix, err)) - return + return false } err = addPerFieldActions(ctx, adapter, entry.Changes, sv.Value, remoteState) if err != nil { logdiag.LogError(ctx, fmt.Errorf("%s: classifying changes: %w", errorPrefix, err)) - return + return false } switch maxAction := getMaxAction(entry.Changes); maxAction { case deployplan.Skip, deployplan.Undefined: entry.Action = deployplan.Bind case deployplan.Update, deployplan.Resize: + // Same guard as the in-state path: an update can only be applied if the + // resource implements DoUpdate. + if maxAction == deployplan.Update && !adapter.HasDoUpdate() { + logdiag.LogError(ctx, fmt.Errorf("%s: resource does not support update action but plan produced update", errorPrefix)) + return false + } entry.Action = deployplan.BindAndUpdate case deployplan.Recreate, deployplan.UpdateWithID: - logdiag.LogError(ctx, buildBindConflictError(errorPrefix, maxAction, entry.Changes, resourceKey)) + logdiag.LogError(ctx, buildBindConflictError(resourceKey, maxAction, entry.Changes, true)) + return false default: logdiag.LogError(ctx, fmt.Errorf("%s: internal error: unexpected action %q during bind planning", errorPrefix, maxAction)) + return false } + return true } // buildBindConflictError formats a multi-line message that names the fields whose // changes forced an incompatible action (Recreate / UpdateWithID) and shows the two -// concrete resolutions: drop the offending fields, or drop the bind block. -func buildBindConflictError(errorPrefix string, action deployplan.ActionType, changes deployplan.Changes, resourceKey string) error { +// concrete resolutions: drop the offending fields, or drop the bind block. firstBind +// tells a resource being adopted apart from one that is already bound in state. +func buildBindConflictError(resourceKey string, action deployplan.ActionType, changes deployplan.Changes, firstBind bool) error { problematicFields := make([]string, 0, len(changes)) for fieldPath, change := range changes { if change.Action == action { @@ -203,17 +219,32 @@ func buildBindConflictError(errorPrefix string, action deployplan.ActionType, ch resourceType, resourceName := splitResourceKey(resourceKey) - var msg strings.Builder + var headline, effect, option1 string // buildBindConflictError is only invoked for Recreate / UpdateWithID; other // action types are guarded by the caller. - switch action { //nolint:exhaustive - case deployplan.Recreate: - msg.WriteString(errorPrefix + ": cannot recreate resource with bind block\n\n") - msg.WriteString("This would destroy and recreate the existing workspace resource, changing its ID.\n\n") - case deployplan.UpdateWithID: - msg.WriteString(errorPrefix + ": cannot update resource ID with bind block\n\n") - msg.WriteString("This would replace the existing workspace resource with a new ID.\n\n") + switch { //nolint:exhaustive + case action == deployplan.Recreate && firstBind: + headline = "cannot bind " + resourceKey + ": cannot recreate resource with bind block" + effect = "This would destroy and recreate the existing workspace resource, changing its ID." + case action == deployplan.Recreate: + headline = "cannot plan " + resourceKey + ": bind block prevents recreating the bound resource" + effect = "This would destroy and recreate the bound workspace resource, changing its ID." + case action == deployplan.UpdateWithID && firstBind: + headline = "cannot bind " + resourceKey + ": cannot update resource ID with bind block" + effect = "This would replace the existing workspace resource with a new ID." + case action == deployplan.UpdateWithID: + headline = "cannot plan " + resourceKey + ": bind block prevents changing the ID of the bound resource" + effect = "This would replace the bound workspace resource with a new ID." } + if firstBind { + option1 = "Remove the problematic fields from your configuration to make this a bind-only operation:" + } else { + option1 = "Revert the problematic fields in your configuration to keep the bound resource as it is:" + } + + var msg strings.Builder + msg.WriteString(headline + "\n\n") + msg.WriteString(effect + "\n\n") msg.WriteString("The following fields cannot be modified because they require ") if action == deployplan.Recreate { @@ -231,7 +262,7 @@ func buildBindConflictError(errorPrefix string, action deployplan.ActionType, ch msg.WriteString("\n") msg.WriteString("To resolve this issue, you have two options:\n\n") - msg.WriteString("1. Remove the problematic fields from your configuration to make this a bind-only operation:\n\n") + msg.WriteString("1. " + option1 + "\n\n") msg.WriteString(" resources:\n") fmt.Fprintf(&msg, " %s:\n", resourceType) fmt.Fprintf(&msg, " %s:\n", resourceName) @@ -247,7 +278,8 @@ func buildBindConflictError(errorPrefix string, action deployplan.ActionType, ch msg.WriteString(" # bind:\n") fmt.Fprintf(&msg, " # %s:\n", resourceType) fmt.Fprintf(&msg, " # %s:\n", resourceName) - msg.WriteString(" # id: \n") + // No trailing newline: error strings must not end with one. + msg.WriteString(" # id: ") return errors.New(msg.String()) } From f5c97abc8fd45122cc48ec4c3cea470fbf5bea70 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:22:32 +0000 Subject: [PATCH 07/20] Warn on unbind when a bind block will re-bind the resource Bind is state-less: nothing records that a resource was just unbound, so a bind block still in config re-adopts it on the next deploy. Say so when 'bundle deployment unbind' runs for a key that has a bind block in the selected target. The new pure-bind test also pins the re-bind round trip itself, the pure 'bind' action (the first deploy of a job is always a bind_and_update because the bundle sets deployment metadata on it), and the single remote read of a pure bind. Co-authored-by: Isaac --- .../deploy/bind/pure-bind/databricks.yml | 26 +++++++++ .../deploy/bind/pure-bind/out.test.toml | 2 + .../bundle/deploy/bind/pure-bind/output.txt | 53 +++++++++++++++++++ .../bundle/deploy/bind/pure-bind/script | 25 +++++++++ .../bundle/deploy/bind/pure-bind/test.toml | 1 + bundle/phases/bind.go | 12 +++++ 6 files changed, 119 insertions(+) create mode 100644 acceptance/bundle/deploy/bind/pure-bind/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/pure-bind/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/pure-bind/output.txt create mode 100644 acceptance/bundle/deploy/bind/pure-bind/script create mode 100644 acceptance/bundle/deploy/bind/pure-bind/test.toml diff --git a/acceptance/bundle/deploy/bind/pure-bind/databricks.yml b/acceptance/bundle/deploy/bind/pure-bind/databricks.yml new file mode 100644 index 00000000000..9c237ba59bf --- /dev/null +++ b/acceptance/bundle/deploy/bind/pure-bind/databricks.yml @@ -0,0 +1,26 @@ +bundle: + name: test-bind-pure + +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: /Workspace/test.py + permissions: + - group_name: users + level: CAN_VIEW + +targets: + default: + bind: + jobs: + foo: + id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/pure-bind/out.test.toml b/acceptance/bundle/deploy/bind/pure-bind/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/pure-bind/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/pure-bind/output.txt b/acceptance/bundle/deploy/bind/pure-bind/output.txt new file mode 100644 index 00000000000..0f056723901 --- /dev/null +++ b/acceptance/bundle/deploy/bind/pure-bind/output.txt @@ -0,0 +1,53 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-pure/default/files... +Bound and updated jobs.foo +Created jobs.foo.permissions +Files: 1 uploaded, 0 deleted +Resources: 1 created, 1 changed, 0 deleted, 0 unchanged, 1 bound + +=== Unbind the job. The bind block is still in config, so the next deploy re-binds it +>>> [CLI] bundle deployment unbind foo +Warning: the bind block for resources.jobs.foo re-binds the resource on the next deploy; remove it to keep the resource unbound + at targets.default.bind.jobs.foo + + +=== The job now matches the config, so this is a pure bind +>>> [CLI] bundle plan +bind jobs.foo (id: [NEW_JOB_ID]) +create jobs.foo.permissions + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged, 1 to bind + +>>> [CLI] bundle plan -o json +json.plan.resources.jobs.foo.bind_id = "[NEW_JOB_ID]"; +json.plan.resources.jobs.foo.action = "bind"; + +>>> print_requests.py --get --oneline //jobs/get +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-pure/default/files... +Bound jobs.foo +Created jobs.foo.permissions +Files: 0 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged, 1 bound + +=== The re-bind deploy reads the job once, at plan time +>>> print_requests.py --get --oneline //jobs/get +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} + +>>> print_state.py +json.state.resources.jobs.foo.__id__ = "[NEW_JOB_ID]"; +json.state.resources.jobs.foo.permissions.__id__ = "/jobs/[NEW_JOB_ID]"; + +>>> [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-pure/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/pure-bind/script b/acceptance/bundle/deploy/bind/pure-bind/script new file mode 100644 index 00000000000..7389ece5538 --- /dev/null +++ b/acceptance/bundle/deploy/bind/pure-bind/script @@ -0,0 +1,25 @@ +# Create a job in the workspace and bind it; the first deploy is a bind_and_update +# because the bundle sets fields such as deployment and edit_mode on every job. +NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" + +trace $CLI bundle deploy --auto-approve + +title "Unbind the job. The bind block is still in config, so the next deploy re-binds it" +trace $CLI bundle deployment unbind foo + +title "The job now matches the config, so this is a pure bind" +trace $CLI bundle plan +trace $CLI bundle plan -o json | gron.py | grep -E 'jobs\.foo\.(action|bind_id)' + +trace print_requests.py --get --oneline //jobs/get +trace $CLI bundle deploy --auto-approve + +title "The re-bind deploy reads the job once, at plan time" +trace print_requests.py --get --oneline //jobs/get +trace print_state.py | gron.py | grep __id__ + +# Cleanup +trace $CLI bundle destroy --auto-approve +rm -f out.requests.txt diff --git a/acceptance/bundle/deploy/bind/pure-bind/test.toml b/acceptance/bundle/deploy/bind/pure-bind/test.toml new file mode 100644 index 00000000000..159efe02696 --- /dev/null +++ b/acceptance/bundle/deploy/bind/pure-bind/test.toml @@ -0,0 +1 @@ +RecordRequests = true diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index 36c4fcf56b4..e5ea9bc8bee 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -15,8 +15,10 @@ import ( "github.com/databricks/cli/bundle/statemgmt" "github.com/databricks/cli/libs/agent" "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/log" "github.com/databricks/cli/libs/logdiag" + "github.com/databricks/cli/libs/structs/structpath" ) type BindOptions struct { @@ -142,4 +144,14 @@ func Unbind(ctx context.Context, b *bundle.Bundle, group, resourceKey string, en } statemgmt.PushResourcesState(ctx, b) + + // Bind is state-less: nothing records that the resource was just unbound, so a + // bind block still in config re-adopts it on the next deploy. + if _, ok := b.BindConfig()[group][resourceKey]; ok { + logdiag.LogDiag(ctx, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("the bind block for %s re-binds the resource on the next deploy; remove it to keep the resource unbound", fullResourceKey), + Paths: structpath.NewPathSlice("targets", b.Config.Bundle.Target, "bind", group, resourceKey), + }) + } } From 36a41d6abfa821d6934894a366e0048c620ec329 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:23:18 +0000 Subject: [PATCH 08/20] Drop the bind check from 'bundle deployment migrate' The command only converts an existing terraform state into a direct state now that the terraform engine is gone; it no longer plans anything, and 'bundle deploy' already migrates automatically with bind blocks present. Rejecting bind blocks here only forced users to remove and re-add them. Also note at the remote-sync call site why it plans without bind blocks. Co-authored-by: Isaac --- .../deploy/bind/block-migrate/databricks.yml | 23 ------------------- .../bundle/deploy/bind/block-migrate/hello.py | 1 - .../deploy/bind/block-migrate/out.test.toml | 2 -- .../deploy/bind/block-migrate/output.txt | 3 --- .../bundle/deploy/bind/block-migrate/script | 2 -- .../deploy/bind/block-migrate/test.toml | 2 -- cmd/bundle/config_remote_sync.go | 2 ++ cmd/bundle/deployment/migrate.go | 8 ------- 8 files changed, 2 insertions(+), 41 deletions(-) delete mode 100644 acceptance/bundle/deploy/bind/block-migrate/databricks.yml delete mode 100644 acceptance/bundle/deploy/bind/block-migrate/hello.py delete mode 100644 acceptance/bundle/deploy/bind/block-migrate/out.test.toml delete mode 100644 acceptance/bundle/deploy/bind/block-migrate/output.txt delete mode 100644 acceptance/bundle/deploy/bind/block-migrate/script delete mode 100644 acceptance/bundle/deploy/bind/block-migrate/test.toml diff --git a/acceptance/bundle/deploy/bind/block-migrate/databricks.yml b/acceptance/bundle/deploy/bind/block-migrate/databricks.yml deleted file mode 100644 index 16cc8b2be6f..00000000000 --- a/acceptance/bundle/deploy/bind/block-migrate/databricks.yml +++ /dev/null @@ -1,23 +0,0 @@ -bundle: - name: test-bind-block-migrate - -resources: - jobs: - foo: - name: test-job - environments: - - environment_key: default - spec: - client: "1" - tasks: - - task_key: my_task - environment_key: default - spark_python_task: - python_file: ./hello.py - -targets: - default: - bind: - jobs: - foo: - id: "12345" diff --git a/acceptance/bundle/deploy/bind/block-migrate/hello.py b/acceptance/bundle/deploy/bind/block-migrate/hello.py deleted file mode 100644 index 11b15b1a458..00000000000 --- a/acceptance/bundle/deploy/bind/block-migrate/hello.py +++ /dev/null @@ -1 +0,0 @@ -print("hello") diff --git a/acceptance/bundle/deploy/bind/block-migrate/out.test.toml b/acceptance/bundle/deploy/bind/block-migrate/out.test.toml deleted file mode 100644 index 0f167effef1..00000000000 --- a/acceptance/bundle/deploy/bind/block-migrate/out.test.toml +++ /dev/null @@ -1,2 +0,0 @@ -Cloud = true -EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/block-migrate/output.txt b/acceptance/bundle/deploy/bind/block-migrate/output.txt deleted file mode 100644 index 6bc78e4ace6..00000000000 --- a/acceptance/bundle/deploy/bind/block-migrate/output.txt +++ /dev/null @@ -1,3 +0,0 @@ - ->>> musterr [CLI] bundle deployment migrate -Error: cannot run 'bundle deployment migrate' when bind blocks are defined in the target configuration; bind blocks are only supported with the direct deployment engine diff --git a/acceptance/bundle/deploy/bind/block-migrate/script b/acceptance/bundle/deploy/bind/block-migrate/script deleted file mode 100644 index 64c538ec319..00000000000 --- a/acceptance/bundle/deploy/bind/block-migrate/script +++ /dev/null @@ -1,2 +0,0 @@ -# Try to run migration with bind blocks - should fail -trace musterr $CLI bundle deployment migrate diff --git a/acceptance/bundle/deploy/bind/block-migrate/test.toml b/acceptance/bundle/deploy/bind/block-migrate/test.toml deleted file mode 100644 index 680c17c1e01..00000000000 --- a/acceptance/bundle/deploy/bind/block-migrate/test.toml +++ /dev/null @@ -1,2 +0,0 @@ -# Migration test does not need engine matrix -[EnvMatrix] diff --git a/cmd/bundle/config_remote_sync.go b/cmd/bundle/config_remote_sync.go index 9285f806ee2..fc100a59fa4 100644 --- a/cmd/bundle/config_remote_sync.go +++ b/cmd/bundle/config_remote_sync.go @@ -90,6 +90,8 @@ Examples: // ProcessBundleRet has already opened the direct state for reading. deployBundle := &b.DeploymentBundle + // Without the target's bind blocks: remote-sync reconciles resources that + // are already in state, and adopting new ones is a deploy-time decision. plan, err := deployBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config, nil) if err != nil { stats.ErrorCategory = protos.BundleConfigRemoteSyncErrorCategoryDetectChangesFailed diff --git a/cmd/bundle/deployment/migrate.go b/cmd/bundle/deployment/migrate.go index f2f4a219668..f329ec7962c 100644 --- a/cmd/bundle/deployment/migrate.go +++ b/cmd/bundle/deployment/migrate.go @@ -89,14 +89,6 @@ to the workspace so that subsequent deploys of this bundle use direct deployment } ctx := cmd.Context() - // `bundle deployment migrate` runs against an existing terraform deployment to - // produce a direct-engine state. Bind is a direct-engine-only feature, so we - // don't have a defined behavior for "migrating" with bind blocks present. Reject - // it and let the user remove the bind blocks, migrate, then add them back. - if b.Target != nil && !b.Target.Bind.IsEmpty() { - return errors.New("cannot run 'bundle deployment migrate' when bind blocks are defined in the target configuration; bind blocks are only supported with the direct deployment engine") - } - if stateDesc.Lineage == "" { cmdio.LogString(ctx, `Error: This command migrates the existing Terraform state file (terraform.tfstate) to a direct deployment state file (resources.json). However, no existing local or remote state was found. From 2071687ceefcf15a69e7e214f022be634bb66b2d Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:24:39 +0000 Subject: [PATCH 09/20] Add acceptance coverage for bind scenarios from the design review - int-id: an unquoted (integer) ID, as written in the design doc, binds. - dev-mode: development mode transforms (rename, paused schedule, tags) apply to the adopted resource and are visible in the plan. - block-removed: removing the bind block is the opt-out; the same immutable change then plans a recreate. - rebind-different-id: a bind block for a resource already in state must carry the state ID. Co-authored-by: Isaac --- .../deploy/bind/block-removed/databricks.yml | 18 +++++++++++++ .../bind/block-removed/databricks_unbound.yml | 14 ++++++++++ .../bundle/deploy/bind/block-removed/nb.sql | 2 ++ .../deploy/bind/block-removed/out.test.toml | 2 ++ .../deploy/bind/block-removed/output.txt | 24 +++++++++++++++++ .../bundle/deploy/bind/block-removed/script | 13 +++++++++ .../deploy/bind/block-removed/test.toml | 1 + .../deploy/bind/dev-mode/databricks.yml | 27 +++++++++++++++++++ .../bundle/deploy/bind/dev-mode/out.test.toml | 2 ++ .../bundle/deploy/bind/dev-mode/output.txt | 14 ++++++++++ acceptance/bundle/deploy/bind/dev-mode/script | 9 +++++++ .../bundle/deploy/bind/int-id/databricks.yml | 23 ++++++++++++++++ .../bundle/deploy/bind/int-id/out.test.toml | 2 ++ .../bundle/deploy/bind/int-id/output.txt | 5 ++++ acceptance/bundle/deploy/bind/int-id/script | 6 +++++ .../bind/rebind-different-id/databricks.yml | 19 +++++++++++++ .../databricks_with_bind.yml | 23 ++++++++++++++++ .../bind/rebind-different-id/out.test.toml | 2 ++ .../bind/rebind-different-id/output.txt | 22 +++++++++++++++ .../deploy/bind/rebind-different-id/script | 12 +++++++++ .../deploy/bind/rebind-different-id/test.toml | 1 + 21 files changed, 241 insertions(+) create mode 100644 acceptance/bundle/deploy/bind/block-removed/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml create mode 100644 acceptance/bundle/deploy/bind/block-removed/nb.sql create mode 100644 acceptance/bundle/deploy/bind/block-removed/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/block-removed/output.txt create mode 100644 acceptance/bundle/deploy/bind/block-removed/script create mode 100644 acceptance/bundle/deploy/bind/block-removed/test.toml create mode 100644 acceptance/bundle/deploy/bind/dev-mode/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/dev-mode/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/dev-mode/output.txt create mode 100644 acceptance/bundle/deploy/bind/dev-mode/script create mode 100644 acceptance/bundle/deploy/bind/int-id/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/int-id/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/int-id/output.txt create mode 100644 acceptance/bundle/deploy/bind/int-id/script create mode 100644 acceptance/bundle/deploy/bind/rebind-different-id/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/rebind-different-id/databricks_with_bind.yml create mode 100644 acceptance/bundle/deploy/bind/rebind-different-id/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/rebind-different-id/output.txt create mode 100644 acceptance/bundle/deploy/bind/rebind-different-id/script create mode 100644 acceptance/bundle/deploy/bind/rebind-different-id/test.toml diff --git a/acceptance/bundle/deploy/bind/block-removed/databricks.yml b/acceptance/bundle/deploy/bind/block-removed/databricks.yml new file mode 100644 index 00000000000..e6c22f6854c --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-removed/databricks.yml @@ -0,0 +1,18 @@ +bundle: + name: test-bind-block-removed + +resources: + pipelines: + foo: + name: test-pipeline + storage: /old/storage/path + libraries: + - notebook: + path: ./nb.sql + +targets: + default: + bind: + pipelines: + foo: + id: "PLACEHOLDER_PIPELINE_ID" diff --git a/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml b/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml new file mode 100644 index 00000000000..539ec170a3e --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml @@ -0,0 +1,14 @@ +bundle: + name: test-bind-block-removed + +resources: + pipelines: + foo: + name: test-pipeline + storage: /new/storage/path + libraries: + - notebook: + path: ./nb.sql + +targets: + default: diff --git a/acceptance/bundle/deploy/bind/block-removed/nb.sql b/acceptance/bundle/deploy/bind/block-removed/nb.sql new file mode 100644 index 00000000000..199ff507884 --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-removed/nb.sql @@ -0,0 +1,2 @@ +-- Databricks notebook source +select 1 diff --git a/acceptance/bundle/deploy/bind/block-removed/out.test.toml b/acceptance/bundle/deploy/bind/block-removed/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-removed/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/block-removed/output.txt b/acceptance/bundle/deploy/bind/block-removed/output.txt new file mode 100644 index 00000000000..2cee5430072 --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-removed/output.txt @@ -0,0 +1,24 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-block-removed/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/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/block-removed/script b/acceptance/bundle/deploy/bind/block-removed/script new file mode 100644 index 00000000000..41072fbc79c --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-removed/script @@ -0,0 +1,13 @@ +# 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 diff --git a/acceptance/bundle/deploy/bind/block-removed/test.toml b/acceptance/bundle/deploy/bind/block-removed/test.toml new file mode 100644 index 00000000000..a65df196368 --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-removed/test.toml @@ -0,0 +1 @@ +Ignore = [".databricks", "databricks_unbound.yml"] diff --git a/acceptance/bundle/deploy/bind/dev-mode/databricks.yml b/acceptance/bundle/deploy/bind/dev-mode/databricks.yml new file mode 100644 index 00000000000..292aaa4005a --- /dev/null +++ b/acceptance/bundle/deploy/bind/dev-mode/databricks.yml @@ -0,0 +1,27 @@ +bundle: + name: test-bind-dev-mode + +resources: + jobs: + foo: + name: test-bind-job + schedule: + quartz_cron_expression: "0 0 12 * * ?" + timezone_id: UTC + environments: + - environment_key: default + spec: + client: "1" + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: /Workspace/test.py + +targets: + default: + mode: development + bind: + jobs: + foo: + id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/dev-mode/out.test.toml b/acceptance/bundle/deploy/bind/dev-mode/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/dev-mode/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/dev-mode/output.txt b/acceptance/bundle/deploy/bind/dev-mode/output.txt new file mode 100644 index 00000000000..36af0f1d8ce --- /dev/null +++ b/acceptance/bundle/deploy/bind/dev-mode/output.txt @@ -0,0 +1,14 @@ + +>>> [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 plan -o json +json.plan.resources.jobs.foo.changes.name.action = "update"; +json.plan.resources.jobs.foo.changes.name.new = "[dev [USERNAME]] test-bind-job"; +json.plan.resources.jobs.foo.changes.name.remote = "test-bind-job"; +json.plan.resources.jobs.foo.changes.schedule.pause_status.action = "update"; +json.plan.resources.jobs.foo.changes.schedule.pause_status.new = "PAUSED"; +json.plan.resources.jobs.foo.changes.tags.action = "update"; +json.plan.resources.jobs.foo.changes.tags.new.dev = "[USERNAME]"; diff --git a/acceptance/bundle/deploy/bind/dev-mode/script b/acceptance/bundle/deploy/bind/dev-mode/script new file mode 100644 index 00000000000..3bdacd14b1a --- /dev/null +++ b/acceptance/bundle/deploy/bind/dev-mode/script @@ -0,0 +1,9 @@ +# Binding in development mode applies the mode's transformations to the adopted +# resource: it is renamed with the [dev ] prefix, its schedule is paused and +# it is tagged. The plan is the only place that shows this before it happens. +NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-job", "schedule": {"quartz_cron_expression": "0 0 12 * * ?", "timezone_id": "UTC"}, "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_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" + +trace $CLI bundle plan +trace $CLI bundle plan -o json | gron.py | grep -E 'changes\.(name|schedule|tags)' diff --git a/acceptance/bundle/deploy/bind/int-id/databricks.yml b/acceptance/bundle/deploy/bind/int-id/databricks.yml new file mode 100644 index 00000000000..8e600295ecd --- /dev/null +++ b/acceptance/bundle/deploy/bind/int-id/databricks.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-int-id + +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: /Workspace/test.py + +targets: + default: + bind: + jobs: + foo: + id: PLACEHOLDER_JOB_ID diff --git a/acceptance/bundle/deploy/bind/int-id/out.test.toml b/acceptance/bundle/deploy/bind/int-id/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/int-id/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/int-id/output.txt b/acceptance/bundle/deploy/bind/int-id/output.txt new file mode 100644 index 00000000000..06ee8a67f28 --- /dev/null +++ b/acceptance/bundle/deploy/bind/int-id/output.txt @@ -0,0 +1,5 @@ + +>>> [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 diff --git a/acceptance/bundle/deploy/bind/int-id/script b/acceptance/bundle/deploy/bind/int-id/script new file mode 100644 index 00000000000..b1c6700e046 --- /dev/null +++ b/acceptance/bundle/deploy/bind/int-id/script @@ -0,0 +1,6 @@ +# An unquoted ID is parsed by YAML as an integer and must normalize to the string ID. +NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" + +trace $CLI bundle plan diff --git a/acceptance/bundle/deploy/bind/rebind-different-id/databricks.yml b/acceptance/bundle/deploy/bind/rebind-different-id/databricks.yml new file mode 100644 index 00000000000..23de6561b8a --- /dev/null +++ b/acceptance/bundle/deploy/bind/rebind-different-id/databricks.yml @@ -0,0 +1,19 @@ +bundle: + name: test-bind-rebind-different-id + +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: /Workspace/test.py + +targets: + default: diff --git a/acceptance/bundle/deploy/bind/rebind-different-id/databricks_with_bind.yml b/acceptance/bundle/deploy/bind/rebind-different-id/databricks_with_bind.yml new file mode 100644 index 00000000000..9e5862ebf46 --- /dev/null +++ b/acceptance/bundle/deploy/bind/rebind-different-id/databricks_with_bind.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-rebind-different-id + +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: /Workspace/test.py + +targets: + default: + bind: + jobs: + foo: + id: "999999999" diff --git a/acceptance/bundle/deploy/bind/rebind-different-id/out.test.toml b/acceptance/bundle/deploy/bind/rebind-different-id/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/rebind-different-id/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/rebind-different-id/output.txt b/acceptance/bundle/deploy/bind/rebind-different-id/output.txt new file mode 100644 index 00000000000..e0eb265d26a --- /dev/null +++ b/acceptance/bundle/deploy/bind/rebind-different-id/output.txt @@ -0,0 +1,22 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-rebind-different-id/default/files... +Created jobs.foo +Files: 2 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +=== A bind block for a resource already in state must carry the state ID; a different ID is not a re-point +>>> musterr [CLI] bundle plan +Error: resources.jobs.foo: resource already bound to ID "[FOO_ID]", cannot bind as "999999999"; remove the bind block or unbind the existing resource + at targets.default.bind.jobs.foo + +Error: bind validation failed + + +>>> [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-rebind-different-id/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/rebind-different-id/script b/acceptance/bundle/deploy/bind/rebind-different-id/script new file mode 100644 index 00000000000..6653c8a6e2a --- /dev/null +++ b/acceptance/bundle/deploy/bind/rebind-different-id/script @@ -0,0 +1,12 @@ +# Deploy foo to create it in state +trace $CLI bundle deploy --auto-approve +JOB_ID=$(read_id.py foo) + +title "A bind block for a resource already in state must carry the state ID; a different ID is not a re-point" +cp databricks.yml databricks.yml.bak +cp databricks_with_bind.yml databricks.yml +trace musterr $CLI bundle plan + +# Cleanup +cp databricks.yml.bak databricks.yml +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deploy/bind/rebind-different-id/test.toml b/acceptance/bundle/deploy/bind/rebind-different-id/test.toml new file mode 100644 index 00000000000..f70b5c78fff --- /dev/null +++ b/acceptance/bundle/deploy/bind/rebind-different-id/test.toml @@ -0,0 +1 @@ +Ignore = [".databricks", "databricks.yml.bak", "databricks_with_bind.yml"] From ed9ee417088cc170b5a57578b69c0c64e8131e45 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:25:45 +0000 Subject: [PATCH 10/20] Document bind semantics and caveats in the schema and changelog Spell out in the schema descriptions what a bind block does on the first deploy (adopt, and update if the config differs, including target mode transforms), what it guards while it remains (delete, recreate), and what it does not (bundle destroy; no marker in state). The regenerated schema also picks up main's current variable pattern for the bind definitions. Co-authored-by: Isaac --- .nextchanges/bundles/4630.md | 2 +- bundle/internal/schema/annotations.yml | 4 ++-- bundle/schema/jsonschema.json | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.nextchanges/bundles/4630.md b/.nextchanges/bundles/4630.md index f0dd7f82b22..efaf572b44f 100644 --- a/.nextchanges/bundles/4630.md +++ b/.nextchanges/bundles/4630.md @@ -1 +1 @@ -* engine/direct: Add declarative `bind` blocks under a target to bring existing workspace resources under bundle management at deploy time, with `bind` and `bind_and_update` actions surfaced in `bundle plan` output ([#4630](https://github.com/databricks/cli/pull/4630)). +* Add declarative `bind` blocks under a target to bring existing workspace resources under bundle management at deploy time. A first deploy adopts the resource (`bind`) and applies the configuration if it differs (`bind_and_update`); while the block remains, deploys cannot delete or recreate the resource, and `bundle destroy` still deletes it ([#4630](https://github.com/databricks/cli/pull/4630)). diff --git a/bundle/internal/schema/annotations.yml b/bundle/internal/schema/annotations.yml index 551c4beef10..b0bff127ef2 100644 --- a/bundle/internal/schema/annotations.yml +++ b/bundle/internal/schema/annotations.yml @@ -2265,11 +2265,11 @@ targets: The artifacts to include in the target deployment. "bind": "description": |- - The existing workspace resources to bind into bundle management for this target. + Existing workspace resources to bring under this target's management, keyed by resource type and by the resource key defined under `resources`. The first deploy adopts each resource (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block remains, deploys can neither delete nor recreate the resource. Removing the block lifts that protection, and `bundle destroy` deletes bound resources like any other; the deployment state does not record that a resource was bound. "$fields": "id": "description": |- - The ID of the existing workspace resource to bind. + The ID of the existing workspace resource to bind, as used by its API (for example a job ID, or the full name of a schema or volume). Variable references such as `${var.job_id}` are resolved. "bundle": "description": |- The bundle attributes when deploying to this target. diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index c875eaedf8a..8c31f667cbc 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -3955,7 +3955,7 @@ "type": "object", "properties": { "id": { - "description": "The ID of the existing workspace resource to bind.", + "description": "The ID of the existing workspace resource to bind, as used by its API (for example a job ID, or the full name of a schema or volume). Variable references such as `${var.job_id}` are resolved.", "$ref": "#/$defs/string" } }, @@ -3966,7 +3966,7 @@ }, { "type": "string", - "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" } ] }, @@ -4512,7 +4512,7 @@ "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config.Artifact" }, "bind": { - "description": "The existing workspace resources to bind into bundle management for this target.", + "description": "Existing workspace resources to bring under this target's management, keyed by resource type and by the resource key defined under `resources`. The first deploy adopts each resource (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block remains, deploys can neither delete nor recreate the resource. Removing the block lifts that protection, and `bundle destroy` deletes bound resources like any other; the deployment state does not record that a resource was bound.", "$ref": "#/$defs/map/map/github.com/databricks/cli/bundle/config.BindResource" }, "bundle": { @@ -22135,7 +22135,7 @@ }, { "type": "string", - "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" } ] }, @@ -22200,7 +22200,7 @@ }, { "type": "string", - "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" } ] } From 977f2e1c3788297f011465df1afdf03a7057fa0b Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:26:22 +0000 Subject: [PATCH 11/20] Add default branches to the bind action switches for the exhaustive linter Co-authored-by: Isaac --- bundle/deployplan/action.go | 5 +++-- bundle/direct/apply.go | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bundle/deployplan/action.go b/bundle/deployplan/action.go index d74dada17e1..e85a5b63df6 100644 --- a/bundle/deployplan/action.go +++ b/bundle/deployplan/action.go @@ -101,9 +101,10 @@ func AppliedLine(resourceKey string, action ActionType) string { return "Bound " + key case BindAndUpdate: return "Bound and updated " + key + default: + verb := action.StringShort() + "d" + return strings.ToUpper(verb[:1]) + verb[1:] + " " + key } - verb := action.StringShort() + "d" - return strings.ToUpper(verb[:1]) + verb[1:] + " " + key } // GetHigherAction returns the action with higher severity between a and b. diff --git a/bundle/direct/apply.go b/bundle/direct/apply.go index a9fe413fb68..9e95c4cebc0 100644 --- a/bundle/direct/apply.go +++ b/bundle/direct/apply.go @@ -43,6 +43,8 @@ func (d *DeploymentUnit) Deploy(ctx context.Context, db *dstate.DeploymentState, return d.Bind(ctx, db, planEntry.BindID, newState, planEntry) case deployplan.BindAndUpdate: return d.BindAndUpdate(ctx, db, planEntry.BindID, newState, planEntry) + default: + // The remaining actions operate on the ID recorded in state. } oldID := db.GetResourceID(d.ResourceKey) From b462efc80500a954bb489f1e5e1f26d32d0e13e4 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:54:56 +0000 Subject: [PATCH 12/20] Move bind under the resource lifecycle block Bind becomes a lifecycle setting of the resource it adopts: resources: jobs: my_job: lifecycle: bind: id: "12345" with per-target IDs set through the usual target override of the resource. This replaces the target-level 'bind' block (design decision DD2 reversed). The block now lives on the resource, so a whole class of misplacements cannot happen: a bind for an undefined resource, a root-level or target-level misplacement that silently created a duplicate, and a stale block whose resource was removed. Variable references, diagnostic locations and target overrides come from the regular config pipeline, which removes the dedicated resolver and validator and the typed target snapshot they worked around. CalculatePlan derives the bind IDs from the config root again. Semantics follow prevent_destroy: the guard against recreating the bound resource holds while the block is in config; removing the resource removes the block with it and the deploy deletes the resource, which the removed-from-config test pins. Duplicate-ID detection is now scoped per resource type, matching the state collision check. Tests: configs moved to the new shape; orphaned-bind, invalid-resource-type, bind-permissions and top-level-bind are gone (not expressible); new target-override test; errors now point at resources...lifecycle.bind.id with file locations. Co-authored-by: Isaac --- .nextchanges/bundles/4630.md | 2 +- .../bundle/deploy/bind/basic/databricks.yml | 8 +- .../bind/bind-and-resize/databricks.yml.tmpl | 8 +- .../bind/bind-and-update/databricks.yml | 8 +- .../bind/bind-permissions/databricks.yml | 21 --- .../bundle/deploy/bind/bind-permissions/nb.py | 2 - .../deploy/bind/bind-permissions/output.txt | 13 -- .../deploy/bind/bind-permissions/script | 3 - .../deploy/bind/block-removed/databricks.yml | 8 +- .../bind/block-removed/databricks_unbound.yml | 3 - .../databricks_conflict.yml | 8 +- .../bind/delete-and-bind-conflict/output.txt | 5 +- .../delete-with-block/databricks_nores.yml | 9 - .../deploy/bind/delete-with-block/output.txt | 29 --- .../deploy/bind/delete-with-block/test.toml | 1 - .../bind/destroy-allowed/databricks.yml | 8 +- .../deploy/bind/dev-mode/databricks.yml | 7 +- .../duplicate-bind-id-config/databricks.yml | 13 +- .../bind/duplicate-bind-id-config/output.txt | 10 +- .../databricks_with_bind.yml | 8 +- .../deploy/bind/duplicate-bind-id/output.txt | 5 +- .../deploy/bind/gone-in-state/databricks.yml | 8 +- .../deploy/bind/gone-in-state/output.txt | 4 +- .../databricks_with_bind.yml | 8 +- .../bundle/deploy/bind/int-id/databricks.yml | 8 +- .../bind/invalid-resource-type/hello.py | 1 - .../bind/invalid-resource-type/out.test.toml | 2 - .../bind/invalid-resource-type/output.txt | 6 - .../deploy/bind/invalid-resource-type/script | 1 - .../deploy/bind/orphaned-bind/databricks.yml | 23 --- .../bundle/deploy/bind/orphaned-bind/hello.py | 1 - .../deploy/bind/orphaned-bind/out.test.toml | 2 - .../deploy/bind/orphaned-bind/output.txt | 17 -- .../bundle/deploy/bind/orphaned-bind/script | 4 - .../deploy/bind/pure-bind/databricks.yml | 8 +- .../bundle/deploy/bind/pure-bind/output.txt | 5 +- .../databricks_with_bind.yml | 8 +- .../bind/rebind-different-id/output.txt | 5 +- .../recreate-blocked-in-state/databricks.yml | 8 +- .../bind/recreate-blocked-in-state/output.txt | 18 +- .../bind/recreate-blocked/databricks.yml | 8 +- .../deploy/bind/recreate-blocked/output.txt | 18 +- .../deploy/bind/ref-to-bound/databricks.yml | 10 +- .../databricks.yml | 10 +- .../removed-from-config/databricks_nores.yml | 2 + .../out.test.toml | 0 .../bind/removed-from-config/output.txt | 26 +++ .../script | 13 +- .../deploy/bind/removed-from-config/test.toml | 1 + .../bind/resource-not-found/databricks.yml | 8 +- .../databricks.yml | 17 +- .../out.test.toml | 0 .../deploy/bind/target-override/output.txt | 12 ++ .../bundle/deploy/bind/target-override/script | 8 + .../deploy/bind/top-level-bind/databricks.yml | 21 --- .../deploy/bind/top-level-bind/hello.py | 1 - .../deploy/bind/top-level-bind/out.test.toml | 2 - .../deploy/bind/top-level-bind/output.txt | 14 -- .../bundle/deploy/bind/top-level-bind/script | 5 - .../deploy/bind/top-level-bind/test.toml | 1 - .../deploy/bind/variable-id/databricks.yml | 8 +- bundle/bundle.go | 8 - bundle/config/bind.go | 73 -------- bundle/config/bind_test.go | 70 -------- .../config/mutator/resolve_bind_references.go | 65 ------- .../mutator/resolve_bind_references_test.go | 73 -------- .../mutator/resolve_variable_references.go | 43 +++-- .../config/mutator/validate_bind_resources.go | 65 ------- .../mutator/validate_bind_resources_test.go | 66 ------- bundle/config/resources/lifecycle.go | 18 ++ bundle/config/root.go | 12 -- bundle/config/target.go | 5 - bundle/direct/bind.go | 4 +- bundle/direct/bundle_plan.go | 26 ++- bundle/direct/bundle_plan_bind.go | 169 +++++++++--------- bundle/internal/schema/annotations.yml | 20 ++- .../validation/generated/required_fields.go | 39 ++++ bundle/phases/bind.go | 13 +- bundle/phases/deploy.go | 2 +- bundle/phases/destroy.go | 10 +- bundle/phases/initialize.go | 8 - bundle/schema/jsonschema.json | 96 ++++------ bundle/statemgmt/direct_migration.go | 3 +- cmd/bundle/config_remote_sync.go | 4 +- 84 files changed, 386 insertions(+), 1007 deletions(-) delete mode 100644 acceptance/bundle/deploy/bind/bind-permissions/databricks.yml delete mode 100644 acceptance/bundle/deploy/bind/bind-permissions/nb.py delete mode 100644 acceptance/bundle/deploy/bind/bind-permissions/output.txt delete mode 100755 acceptance/bundle/deploy/bind/bind-permissions/script delete mode 100644 acceptance/bundle/deploy/bind/delete-with-block/databricks_nores.yml delete mode 100644 acceptance/bundle/deploy/bind/delete-with-block/output.txt delete mode 100644 acceptance/bundle/deploy/bind/delete-with-block/test.toml delete mode 100644 acceptance/bundle/deploy/bind/invalid-resource-type/hello.py delete mode 100644 acceptance/bundle/deploy/bind/invalid-resource-type/out.test.toml delete mode 100644 acceptance/bundle/deploy/bind/invalid-resource-type/output.txt delete mode 100644 acceptance/bundle/deploy/bind/invalid-resource-type/script delete mode 100644 acceptance/bundle/deploy/bind/orphaned-bind/databricks.yml delete mode 100644 acceptance/bundle/deploy/bind/orphaned-bind/hello.py delete mode 100644 acceptance/bundle/deploy/bind/orphaned-bind/out.test.toml delete mode 100644 acceptance/bundle/deploy/bind/orphaned-bind/output.txt delete mode 100644 acceptance/bundle/deploy/bind/orphaned-bind/script rename acceptance/bundle/deploy/bind/{delete-with-block => removed-from-config}/databricks.yml (78%) create mode 100644 acceptance/bundle/deploy/bind/removed-from-config/databricks_nores.yml rename acceptance/bundle/deploy/bind/{bind-permissions => removed-from-config}/out.test.toml (100%) create mode 100644 acceptance/bundle/deploy/bind/removed-from-config/output.txt rename acceptance/bundle/deploy/bind/{delete-with-block => removed-from-config}/script (57%) create mode 100644 acceptance/bundle/deploy/bind/removed-from-config/test.toml rename acceptance/bundle/deploy/bind/{invalid-resource-type => target-override}/databricks.yml (50%) rename acceptance/bundle/deploy/bind/{delete-with-block => target-override}/out.test.toml (100%) create mode 100644 acceptance/bundle/deploy/bind/target-override/output.txt create mode 100644 acceptance/bundle/deploy/bind/target-override/script delete mode 100644 acceptance/bundle/deploy/bind/top-level-bind/databricks.yml delete mode 100644 acceptance/bundle/deploy/bind/top-level-bind/hello.py delete mode 100644 acceptance/bundle/deploy/bind/top-level-bind/out.test.toml delete mode 100644 acceptance/bundle/deploy/bind/top-level-bind/output.txt delete mode 100644 acceptance/bundle/deploy/bind/top-level-bind/script delete mode 100644 acceptance/bundle/deploy/bind/top-level-bind/test.toml delete mode 100644 bundle/config/bind.go delete mode 100644 bundle/config/bind_test.go delete mode 100644 bundle/config/mutator/resolve_bind_references.go delete mode 100644 bundle/config/mutator/resolve_bind_references_test.go delete mode 100644 bundle/config/mutator/validate_bind_resources.go delete mode 100644 bundle/config/mutator/validate_bind_resources_test.go diff --git a/.nextchanges/bundles/4630.md b/.nextchanges/bundles/4630.md index efaf572b44f..142b2b74d5d 100644 --- a/.nextchanges/bundles/4630.md +++ b/.nextchanges/bundles/4630.md @@ -1 +1 @@ -* Add declarative `bind` blocks under a target to bring existing workspace resources under bundle management at deploy time. A first deploy adopts the resource (`bind`) and applies the configuration if it differs (`bind_and_update`); while the block remains, deploys cannot delete or recreate the resource, and `bundle destroy` still deletes it ([#4630](https://github.com/databricks/cli/pull/4630)). +* 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)). diff --git a/acceptance/bundle/deploy/bind/basic/databricks.yml b/acceptance/bundle/deploy/bind/basic/databricks.yml index a6b280560ab..3891ee43639 100644 --- a/acceptance/bundle/deploy/bind/basic/databricks.yml +++ b/acceptance/bundle/deploy/bind/basic/databricks.yml @@ -14,10 +14,6 @@ resources: environment_key: default spark_python_task: python_file: ./hello.py - -targets: - default: - bind: - jobs: - foo: + lifecycle: + bind: id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl b/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl index 95b9638eafe..8118cc18b4c 100644 --- a/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl +++ b/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl @@ -12,10 +12,6 @@ resources: node_type_id: $NODE_TYPE_ID instance_pool_id: $TEST_INSTANCE_POOL_ID num_workers: 3 - -targets: - default: - bind: - clusters: - test_cluster: + lifecycle: + bind: id: "PLACEHOLDER_CLUSTER_ID" diff --git a/acceptance/bundle/deploy/bind/bind-and-update/databricks.yml b/acceptance/bundle/deploy/bind/bind-and-update/databricks.yml index 3b813772d02..b6b2980dd85 100644 --- a/acceptance/bundle/deploy/bind/bind-and-update/databricks.yml +++ b/acceptance/bundle/deploy/bind/bind-and-update/databricks.yml @@ -14,10 +14,6 @@ resources: environment_key: default spark_python_task: python_file: ./hello.py - -targets: - default: - bind: - jobs: - foo: + lifecycle: + bind: id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/bind-permissions/databricks.yml b/acceptance/bundle/deploy/bind/bind-permissions/databricks.yml deleted file mode 100644 index ff4bc4534d3..00000000000 --- a/acceptance/bundle/deploy/bind/bind-permissions/databricks.yml +++ /dev/null @@ -1,21 +0,0 @@ -bundle: - name: test-bind-permissions - -resources: - jobs: - foo: - name: test-job - tasks: - - task_key: test - notebook_task: - notebook_path: ./nb.py - permissions: - - group_name: users - level: CAN_MANAGE - -targets: - default: - bind: - jobs.permissions: - foo: - id: "12345" diff --git a/acceptance/bundle/deploy/bind/bind-permissions/nb.py b/acceptance/bundle/deploy/bind/bind-permissions/nb.py deleted file mode 100644 index 38d86b79c70..00000000000 --- a/acceptance/bundle/deploy/bind/bind-permissions/nb.py +++ /dev/null @@ -1,2 +0,0 @@ -# Databricks notebook source -print("Hello, World!") diff --git a/acceptance/bundle/deploy/bind/bind-permissions/output.txt b/acceptance/bundle/deploy/bind/bind-permissions/output.txt deleted file mode 100644 index 1150649418d..00000000000 --- a/acceptance/bundle/deploy/bind/bind-permissions/output.txt +++ /dev/null @@ -1,13 +0,0 @@ - ->>> musterr [CLI] bundle validate -Error: binding "jobs.permissions" is not supported - -"jobs.permissions" is not a supported resource type; bind is only supported for resources that can be defined under the resources block. - -Name: test-bind-permissions -Target: default -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/test-bind-permissions/default - -Found 1 error diff --git a/acceptance/bundle/deploy/bind/bind-permissions/script b/acceptance/bundle/deploy/bind/bind-permissions/script deleted file mode 100755 index a34e6208ad9..00000000000 --- a/acceptance/bundle/deploy/bind/bind-permissions/script +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -trace musterr $CLI bundle validate diff --git a/acceptance/bundle/deploy/bind/block-removed/databricks.yml b/acceptance/bundle/deploy/bind/block-removed/databricks.yml index e6c22f6854c..10172bca10b 100644 --- a/acceptance/bundle/deploy/bind/block-removed/databricks.yml +++ b/acceptance/bundle/deploy/bind/block-removed/databricks.yml @@ -9,10 +9,6 @@ resources: libraries: - notebook: path: ./nb.sql - -targets: - default: - bind: - pipelines: - foo: + lifecycle: + bind: id: "PLACEHOLDER_PIPELINE_ID" diff --git a/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml b/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml index 539ec170a3e..5f2d3861c56 100644 --- a/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml +++ b/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml @@ -9,6 +9,3 @@ resources: libraries: - notebook: path: ./nb.sql - -targets: - default: diff --git a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks_conflict.yml b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks_conflict.yml index 59758817069..0f6b6845c76 100644 --- a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks_conflict.yml +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks_conflict.yml @@ -14,10 +14,6 @@ resources: environment_key: default spark_python_task: python_file: ./hello.py - -targets: - default: - bind: - jobs: - bar: + lifecycle: + bind: id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/output.txt b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/output.txt index f6ca7b4088f..8e51004004d 100644 --- a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/output.txt +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/output.txt @@ -6,8 +6,9 @@ Files: 3 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged >>> musterr [CLI] bundle plan -Error: bind block for "resources.jobs.bar" has the same ID "[FOO_ID]" as existing resource "resources.jobs.foo"; remove the bind block or the conflicting resource - at targets.default.bind.jobs.bar +Error: resources.jobs.bar has the same lifecycle.bind ID "[FOO_ID]" as existing resource "resources.jobs.foo"; remove lifecycle.bind or the conflicting resource + at resources.jobs.bar.lifecycle.bind.id + in databricks.yml:19:15 Error: bind validation failed diff --git a/acceptance/bundle/deploy/bind/delete-with-block/databricks_nores.yml b/acceptance/bundle/deploy/bind/delete-with-block/databricks_nores.yml deleted file mode 100644 index ce29e0f57c7..00000000000 --- a/acceptance/bundle/deploy/bind/delete-with-block/databricks_nores.yml +++ /dev/null @@ -1,9 +0,0 @@ -bundle: - name: test-bind-delete-with-block - -targets: - default: - bind: - jobs: - foo: - id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/delete-with-block/output.txt b/acceptance/bundle/deploy/bind/delete-with-block/output.txt deleted file mode 100644 index fc12312a182..00000000000 --- a/acceptance/bundle/deploy/bind/delete-with-block/output.txt +++ /dev/null @@ -1,29 +0,0 @@ - ->>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-delete-with-block/default/files... -Bound and updated jobs.foo -Files: 2 uploaded, 0 deleted -Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound - -=== Remove the resource from config but keep its bind block: the block must prevent the delete ->>> musterr [CLI] bundle plan -Error: bind block references undefined resource "resources.jobs.foo"; define it in the resources section or remove the bind block - at targets.default.bind.jobs.foo - - ->>> musterr [CLI] bundle deploy --auto-approve -Error: bind block references undefined resource "resources.jobs.foo"; define it in the resources section or remove the bind block - at targets.default.bind.jobs.foo - - -=== The bound job still exists ->>> [CLI] jobs get [NEW_JOB_ID] -test-bind-job - ->>> [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-delete-with-block/default - -Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/delete-with-block/test.toml b/acceptance/bundle/deploy/bind/delete-with-block/test.toml deleted file mode 100644 index 284fca57d4b..00000000000 --- a/acceptance/bundle/deploy/bind/delete-with-block/test.toml +++ /dev/null @@ -1 +0,0 @@ -Ignore = [".databricks", "databricks.yml.bak", "databricks_nores.yml"] diff --git a/acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml b/acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml index 0a0cf4c7693..8abdbe2c8ca 100644 --- a/acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml +++ b/acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml @@ -14,10 +14,6 @@ resources: environment_key: default spark_python_task: python_file: ./hello.py - -targets: - default: - bind: - jobs: - foo: + lifecycle: + bind: id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/dev-mode/databricks.yml b/acceptance/bundle/deploy/bind/dev-mode/databricks.yml index 292aaa4005a..723742f381a 100644 --- a/acceptance/bundle/deploy/bind/dev-mode/databricks.yml +++ b/acceptance/bundle/deploy/bind/dev-mode/databricks.yml @@ -17,11 +17,10 @@ resources: environment_key: default spark_python_task: python_file: /Workspace/test.py + lifecycle: + bind: + id: "PLACEHOLDER_JOB_ID" targets: default: mode: development - bind: - jobs: - foo: - id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id-config/databricks.yml b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/databricks.yml index 528d389e53a..3820ee5551e 100644 --- a/acceptance/bundle/deploy/bind/duplicate-bind-id-config/databricks.yml +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/databricks.yml @@ -14,6 +14,9 @@ resources: environment_key: default spark_python_task: python_file: ./hello.py + lifecycle: + bind: + id: "12345" bar: name: test-bind-dup-config-bar environments: @@ -25,12 +28,6 @@ resources: environment_key: default spark_python_task: python_file: ./hello.py - -targets: - default: - bind: - jobs: - foo: - id: "12345" - bar: + lifecycle: + bind: id: "12345" diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id-config/output.txt b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/output.txt index 07b7ec4b6ed..6f22c9a60cb 100644 --- a/acceptance/bundle/deploy/bind/duplicate-bind-id-config/output.txt +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/output.txt @@ -1,10 +1,12 @@ >>> musterr [CLI] bundle plan -Error: bind block for "resources.jobs.bar" has the same ID "12345" as bind block(s) for resources.jobs.foo; each workspace resource can only be bound once - at targets.default.bind.jobs.bar +Error: resources.jobs.bar has the same lifecycle.bind ID "12345" as resources.jobs.foo; each workspace resource can only be bound once + at resources.jobs.bar.lifecycle.bind.id + in databricks.yml:33:15 -Error: bind block for "resources.jobs.foo" has the same ID "12345" as bind block(s) for resources.jobs.bar; each workspace resource can only be bound once - at targets.default.bind.jobs.foo +Error: resources.jobs.foo has the same lifecycle.bind ID "12345" as resources.jobs.bar; each workspace resource can only be bound once + at resources.jobs.foo.lifecycle.bind.id + in databricks.yml:19:15 Error: bind validation failed diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks_with_bind.yml b/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks_with_bind.yml index 0b3abd007f2..f852ac6c9a0 100644 --- a/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks_with_bind.yml +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks_with_bind.yml @@ -25,10 +25,6 @@ resources: environment_key: default spark_python_task: python_file: ./hello.py - -targets: - default: - bind: - jobs: - bar: + lifecycle: + bind: id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id/output.txt b/acceptance/bundle/deploy/bind/duplicate-bind-id/output.txt index f573ef27a79..b24d715a924 100644 --- a/acceptance/bundle/deploy/bind/duplicate-bind-id/output.txt +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/output.txt @@ -6,8 +6,9 @@ Files: 3 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged >>> musterr [CLI] bundle plan -Error: bind block for "resources.jobs.bar" has the same ID "[FOO_ID]" as existing resource "resources.jobs.foo"; remove the bind block or the conflicting resource - at targets.default.bind.jobs.bar +Error: resources.jobs.bar has the same lifecycle.bind ID "[FOO_ID]" as existing resource "resources.jobs.foo"; remove lifecycle.bind or the conflicting resource + at resources.jobs.bar.lifecycle.bind.id + in databricks.yml:30:15 Error: bind validation failed diff --git a/acceptance/bundle/deploy/bind/gone-in-state/databricks.yml b/acceptance/bundle/deploy/bind/gone-in-state/databricks.yml index 307c3fad567..f5df89d5945 100644 --- a/acceptance/bundle/deploy/bind/gone-in-state/databricks.yml +++ b/acceptance/bundle/deploy/bind/gone-in-state/databricks.yml @@ -14,10 +14,6 @@ resources: environment_key: default spark_python_task: python_file: /Workspace/test.py - -targets: - default: - bind: - jobs: - foo: + lifecycle: + bind: id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/gone-in-state/output.txt b/acceptance/bundle/deploy/bind/gone-in-state/output.txt index df199fd1a71..6747e7d05a1 100644 --- a/acceptance/bundle/deploy/bind/gone-in-state/output.txt +++ b/acceptance/bundle/deploy/bind/gone-in-state/output.txt @@ -9,14 +9,14 @@ Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound >>> [CLI] jobs delete [NEW_JOB_ID] >>> musterr [CLI] bundle plan -Error: cannot plan resources.jobs.foo: bound resource with ID "[NEW_JOB_ID]" no longer exists in the workspace; remove the bind block to create a new resource +Error: cannot plan resources.jobs.foo: bound resource with ID "[NEW_JOB_ID]" no longer exists in the workspace; remove lifecycle.bind to create a new resource Error: planning failed >>> musterr [CLI] bundle deploy --auto-approve Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-gone-in-state/default/files... -Error: cannot plan resources.jobs.foo: bound resource with ID "[NEW_JOB_ID]" no longer exists in the workspace; remove the bind block to create a new resource +Error: cannot plan resources.jobs.foo: bound resource with ID "[NEW_JOB_ID]" no longer exists in the workspace; remove lifecycle.bind to create a new resource Error: planning failed diff --git a/acceptance/bundle/deploy/bind/id-collision-different-type/databricks_with_bind.yml b/acceptance/bundle/deploy/bind/id-collision-different-type/databricks_with_bind.yml index 88fca7da067..6e5af0ee0db 100644 --- a/acceptance/bundle/deploy/bind/id-collision-different-type/databricks_with_bind.yml +++ b/acceptance/bundle/deploy/bind/id-collision-different-type/databricks_with_bind.yml @@ -17,10 +17,6 @@ resources: experiments: exp: name: /Shared/test-bind-collision-exp - -targets: - default: - bind: - experiments: - exp: + lifecycle: + bind: id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/int-id/databricks.yml b/acceptance/bundle/deploy/bind/int-id/databricks.yml index 8e600295ecd..b8d623b657c 100644 --- a/acceptance/bundle/deploy/bind/int-id/databricks.yml +++ b/acceptance/bundle/deploy/bind/int-id/databricks.yml @@ -14,10 +14,6 @@ resources: environment_key: default spark_python_task: python_file: /Workspace/test.py - -targets: - default: - bind: - jobs: - foo: + lifecycle: + bind: id: PLACEHOLDER_JOB_ID diff --git a/acceptance/bundle/deploy/bind/invalid-resource-type/hello.py b/acceptance/bundle/deploy/bind/invalid-resource-type/hello.py deleted file mode 100644 index 11b15b1a458..00000000000 --- a/acceptance/bundle/deploy/bind/invalid-resource-type/hello.py +++ /dev/null @@ -1 +0,0 @@ -print("hello") diff --git a/acceptance/bundle/deploy/bind/invalid-resource-type/out.test.toml b/acceptance/bundle/deploy/bind/invalid-resource-type/out.test.toml deleted file mode 100644 index 0f167effef1..00000000000 --- a/acceptance/bundle/deploy/bind/invalid-resource-type/out.test.toml +++ /dev/null @@ -1,2 +0,0 @@ -Cloud = true -EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/invalid-resource-type/output.txt b/acceptance/bundle/deploy/bind/invalid-resource-type/output.txt deleted file mode 100644 index eb33b3f42ba..00000000000 --- a/acceptance/bundle/deploy/bind/invalid-resource-type/output.txt +++ /dev/null @@ -1,6 +0,0 @@ - ->>> musterr [CLI] bundle plan -Error: binding "foobar" is not supported - -"foobar" is not a supported resource type; bind is only supported for resources that can be defined under the resources block. - diff --git a/acceptance/bundle/deploy/bind/invalid-resource-type/script b/acceptance/bundle/deploy/bind/invalid-resource-type/script deleted file mode 100644 index 9d9604578f1..00000000000 --- a/acceptance/bundle/deploy/bind/invalid-resource-type/script +++ /dev/null @@ -1 +0,0 @@ -trace musterr $CLI bundle plan diff --git a/acceptance/bundle/deploy/bind/orphaned-bind/databricks.yml b/acceptance/bundle/deploy/bind/orphaned-bind/databricks.yml deleted file mode 100644 index 26d0272b267..00000000000 --- a/acceptance/bundle/deploy/bind/orphaned-bind/databricks.yml +++ /dev/null @@ -1,23 +0,0 @@ -bundle: - name: test-bind-orphaned - -resources: - jobs: - foo: - name: test-job - environments: - - environment_key: default - spec: - client: "1" - tasks: - - task_key: my_task - environment_key: default - spark_python_task: - python_file: ./hello.py - -targets: - default: - bind: - jobs: - bar: - id: "12345" diff --git a/acceptance/bundle/deploy/bind/orphaned-bind/hello.py b/acceptance/bundle/deploy/bind/orphaned-bind/hello.py deleted file mode 100644 index 11b15b1a458..00000000000 --- a/acceptance/bundle/deploy/bind/orphaned-bind/hello.py +++ /dev/null @@ -1 +0,0 @@ -print("hello") diff --git a/acceptance/bundle/deploy/bind/orphaned-bind/out.test.toml b/acceptance/bundle/deploy/bind/orphaned-bind/out.test.toml deleted file mode 100644 index 0f167effef1..00000000000 --- a/acceptance/bundle/deploy/bind/orphaned-bind/out.test.toml +++ /dev/null @@ -1,2 +0,0 @@ -Cloud = true -EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/orphaned-bind/output.txt b/acceptance/bundle/deploy/bind/orphaned-bind/output.txt deleted file mode 100644 index 197b527da3f..00000000000 --- a/acceptance/bundle/deploy/bind/orphaned-bind/output.txt +++ /dev/null @@ -1,17 +0,0 @@ - ->>> musterr [CLI] bundle validate -Error: bind block references undefined resource "resources.jobs.bar"; define it in the resources section or remove the bind block - at targets.default.bind.jobs.bar - -Name: test-bind-orphaned -Target: default -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/test-bind-orphaned/default - -Found 1 error - ->>> musterr [CLI] bundle plan -Error: bind block references undefined resource "resources.jobs.bar"; define it in the resources section or remove the bind block - at targets.default.bind.jobs.bar - diff --git a/acceptance/bundle/deploy/bind/orphaned-bind/script b/acceptance/bundle/deploy/bind/orphaned-bind/script deleted file mode 100644 index 5e2f41094d1..00000000000 --- a/acceptance/bundle/deploy/bind/orphaned-bind/script +++ /dev/null @@ -1,4 +0,0 @@ -# Bind block references jobs.bar but only jobs.foo exists in resources. -# This is caught at validation time, before any plan is attempted. -trace musterr $CLI bundle validate -trace musterr $CLI bundle plan diff --git a/acceptance/bundle/deploy/bind/pure-bind/databricks.yml b/acceptance/bundle/deploy/bind/pure-bind/databricks.yml index 9c237ba59bf..2d23ceb407c 100644 --- a/acceptance/bundle/deploy/bind/pure-bind/databricks.yml +++ b/acceptance/bundle/deploy/bind/pure-bind/databricks.yml @@ -17,10 +17,6 @@ resources: permissions: - group_name: users level: CAN_VIEW - -targets: - default: - bind: - jobs: - foo: + lifecycle: + bind: id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/pure-bind/output.txt b/acceptance/bundle/deploy/bind/pure-bind/output.txt index 0f056723901..32e0f9dd517 100644 --- a/acceptance/bundle/deploy/bind/pure-bind/output.txt +++ b/acceptance/bundle/deploy/bind/pure-bind/output.txt @@ -8,8 +8,9 @@ Resources: 1 created, 1 changed, 0 deleted, 0 unchanged, 1 bound === Unbind the job. The bind block is still in config, so the next deploy re-binds it >>> [CLI] bundle deployment unbind foo -Warning: the bind block for resources.jobs.foo re-binds the resource on the next deploy; remove it to keep the resource unbound - at targets.default.bind.jobs.foo +Warning: resources.jobs.foo.lifecycle.bind re-binds the resource on the next deploy; remove it to keep the resource unbound + at resources.jobs.foo.lifecycle.bind + in databricks.yml:22:11 === The job now matches the config, so this is a pure bind diff --git a/acceptance/bundle/deploy/bind/rebind-different-id/databricks_with_bind.yml b/acceptance/bundle/deploy/bind/rebind-different-id/databricks_with_bind.yml index 9e5862ebf46..459b71e1445 100644 --- a/acceptance/bundle/deploy/bind/rebind-different-id/databricks_with_bind.yml +++ b/acceptance/bundle/deploy/bind/rebind-different-id/databricks_with_bind.yml @@ -14,10 +14,6 @@ resources: environment_key: default spark_python_task: python_file: /Workspace/test.py - -targets: - default: - bind: - jobs: - foo: + lifecycle: + bind: id: "999999999" diff --git a/acceptance/bundle/deploy/bind/rebind-different-id/output.txt b/acceptance/bundle/deploy/bind/rebind-different-id/output.txt index e0eb265d26a..a21c3ab9b89 100644 --- a/acceptance/bundle/deploy/bind/rebind-different-id/output.txt +++ b/acceptance/bundle/deploy/bind/rebind-different-id/output.txt @@ -7,8 +7,9 @@ Resources: 1 created, 0 changed, 0 deleted, 0 unchanged === A bind block for a resource already in state must carry the state ID; a different ID is not a re-point >>> musterr [CLI] bundle plan -Error: resources.jobs.foo: resource already bound to ID "[FOO_ID]", cannot bind as "999999999"; remove the bind block or unbind the existing resource - at targets.default.bind.jobs.foo +Error: resources.jobs.foo: resource already bound to ID "[FOO_ID]", cannot bind as "999999999"; remove lifecycle.bind or unbind the existing resource + at resources.jobs.foo.lifecycle.bind.id + in databricks.yml:19:15 Error: bind validation failed diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/databricks.yml b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/databricks.yml index b55e1aec196..c8385967447 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/databricks.yml +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/databricks.yml @@ -9,10 +9,6 @@ resources: libraries: - notebook: path: ./nb.sql - -targets: - default: - bind: - pipelines: - foo: + lifecycle: + bind: id: "PLACEHOLDER_PIPELINE_ID" diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt index 3100e00ee3f..d9b479d7054 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt @@ -6,7 +6,7 @@ Files: 2 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound >>> musterr [CLI] bundle plan -Error: cannot plan resources.pipelines.foo: bind block prevents recreating the bound resource +Error: cannot plan resources.pipelines.foo: lifecycle.bind prevents recreating the bound resource This would destroy and recreate the bound workspace resource, changing its ID. @@ -23,15 +23,15 @@ To resolve this issue, you have two options: # Remove or comment out these fields: # storage: ... -2. Remove the bind block if you want to allow the resource to be recreated/updated: +2. Remove lifecycle.bind if you want to allow the resource to be recreated/updated: - targets: - : - # Remove the bind block: - # bind: - # pipelines: - # foo: - # id: + resources: + pipelines: + foo: + lifecycle: + # Remove the bind block: + # bind: + # id: Error: planning failed diff --git a/acceptance/bundle/deploy/bind/recreate-blocked/databricks.yml b/acceptance/bundle/deploy/bind/recreate-blocked/databricks.yml index ddad62da6ad..7afeed66ed2 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked/databricks.yml +++ b/acceptance/bundle/deploy/bind/recreate-blocked/databricks.yml @@ -9,10 +9,6 @@ resources: libraries: - notebook: path: ./nb.sql - -targets: - default: - bind: - pipelines: - foo: + lifecycle: + bind: id: "PLACEHOLDER_PIPELINE_ID" diff --git a/acceptance/bundle/deploy/bind/recreate-blocked/output.txt b/acceptance/bundle/deploy/bind/recreate-blocked/output.txt index ecf96ca9021..51588345908 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked/output.txt +++ b/acceptance/bundle/deploy/bind/recreate-blocked/output.txt @@ -1,6 +1,6 @@ >>> musterr [CLI] bundle plan -Error: cannot bind resources.pipelines.foo: cannot recreate resource with bind block +Error: cannot bind resources.pipelines.foo: cannot recreate resource with lifecycle.bind This would destroy and recreate the existing workspace resource, changing its ID. @@ -17,15 +17,15 @@ To resolve this issue, you have two options: # Remove or comment out these fields: # storage: ... -2. Remove the bind block if you want to allow the resource to be recreated/updated: +2. Remove lifecycle.bind if you want to allow the resource to be recreated/updated: - targets: - : - # Remove the bind block: - # bind: - # pipelines: - # foo: - # id: + resources: + pipelines: + foo: + lifecycle: + # Remove the bind block: + # bind: + # id: Error: planning failed diff --git a/acceptance/bundle/deploy/bind/ref-to-bound/databricks.yml b/acceptance/bundle/deploy/bind/ref-to-bound/databricks.yml index 9bda212160e..f922fdc4e9b 100644 --- a/acceptance/bundle/deploy/bind/ref-to-bound/databricks.yml +++ b/acceptance/bundle/deploy/bind/ref-to-bound/databricks.yml @@ -14,16 +14,12 @@ resources: environment_key: default spark_python_task: python_file: /Workspace/test.py + lifecycle: + bind: + id: "PLACEHOLDER_JOB_ID" bar: name: test-bind-runner tasks: - task_key: run_foo run_job_task: job_id: ${resources.jobs.foo.id} - -targets: - default: - bind: - jobs: - foo: - id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/delete-with-block/databricks.yml b/acceptance/bundle/deploy/bind/removed-from-config/databricks.yml similarity index 78% rename from acceptance/bundle/deploy/bind/delete-with-block/databricks.yml rename to acceptance/bundle/deploy/bind/removed-from-config/databricks.yml index 768501ce455..ff2cbca35cb 100644 --- a/acceptance/bundle/deploy/bind/delete-with-block/databricks.yml +++ b/acceptance/bundle/deploy/bind/removed-from-config/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-delete-with-block + name: test-bind-removed-from-config resources: jobs: @@ -14,10 +14,6 @@ resources: environment_key: default spark_python_task: python_file: /Workspace/test.py - -targets: - default: - bind: - jobs: - foo: + lifecycle: + bind: id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/removed-from-config/databricks_nores.yml b/acceptance/bundle/deploy/bind/removed-from-config/databricks_nores.yml new file mode 100644 index 00000000000..6e007493827 --- /dev/null +++ b/acceptance/bundle/deploy/bind/removed-from-config/databricks_nores.yml @@ -0,0 +1,2 @@ +bundle: + name: test-bind-removed-from-config diff --git a/acceptance/bundle/deploy/bind/bind-permissions/out.test.toml b/acceptance/bundle/deploy/bind/removed-from-config/out.test.toml similarity index 100% rename from acceptance/bundle/deploy/bind/bind-permissions/out.test.toml rename to acceptance/bundle/deploy/bind/removed-from-config/out.test.toml diff --git a/acceptance/bundle/deploy/bind/removed-from-config/output.txt b/acceptance/bundle/deploy/bind/removed-from-config/output.txt new file mode 100644 index 00000000000..7562bbb76d7 --- /dev/null +++ b/acceptance/bundle/deploy/bind/removed-from-config/output.txt @@ -0,0 +1,26 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-removed-from-config/default/files... +Bound and updated jobs.foo +Files: 2 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound + +=== Remove the resource from config. Its lifecycle.bind goes with it, so, as with prevent_destroy, the protection ends and the deploy deletes the job +>>> [CLI] bundle plan +delete jobs.foo + +Plan: 0 to add, 0 to change, 1 to delete, 0 unchanged + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-removed-from-config/default/files... +Deleted jobs.foo +Files: 1 uploaded, 0 deleted +Resources: 0 created, 0 changed, 1 deleted, 0 unchanged + +>>> musterr [CLI] jobs get [NEW_JOB_ID] +Error: Job [NEW_JOB_ID] does not exist. + +>>> [CLI] bundle destroy --auto-approve +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-removed-from-config/default + +Destroy: 0 deleted diff --git a/acceptance/bundle/deploy/bind/delete-with-block/script b/acceptance/bundle/deploy/bind/removed-from-config/script similarity index 57% rename from acceptance/bundle/deploy/bind/delete-with-block/script rename to acceptance/bundle/deploy/bind/removed-from-config/script index 407b9c80149..1eddff9c0b7 100644 --- a/acceptance/bundle/deploy/bind/delete-with-block/script +++ b/acceptance/bundle/deploy/bind/removed-from-config/script @@ -2,19 +2,14 @@ NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" -update_file.py databricks_nores.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" trace $CLI bundle deploy --auto-approve -title "Remove the resource from config but keep its bind block: the block must prevent the delete" -cp databricks.yml databricks.yml.bak +title "Remove the resource from config. Its lifecycle.bind goes with it, so, as with prevent_destroy, the protection ends and the deploy deletes the job" cp databricks_nores.yml databricks.yml -trace musterr $CLI bundle plan -trace musterr $CLI bundle deploy --auto-approve - -title "The bound job still exists" -trace $CLI jobs get $NEW_JOB_ID | jq -r .settings.name +trace $CLI bundle plan +trace $CLI bundle deploy --auto-approve +trace musterr $CLI jobs get $NEW_JOB_ID # Cleanup -cp databricks.yml.bak databricks.yml trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deploy/bind/removed-from-config/test.toml b/acceptance/bundle/deploy/bind/removed-from-config/test.toml new file mode 100644 index 00000000000..4c1cffe2567 --- /dev/null +++ b/acceptance/bundle/deploy/bind/removed-from-config/test.toml @@ -0,0 +1 @@ +Ignore = [".databricks", "databricks_nores.yml"] diff --git a/acceptance/bundle/deploy/bind/resource-not-found/databricks.yml b/acceptance/bundle/deploy/bind/resource-not-found/databricks.yml index e02258511da..02196311478 100644 --- a/acceptance/bundle/deploy/bind/resource-not-found/databricks.yml +++ b/acceptance/bundle/deploy/bind/resource-not-found/databricks.yml @@ -14,10 +14,6 @@ resources: environment_key: default spark_python_task: python_file: ./hello.py - -targets: - default: - bind: - jobs: - foo: + lifecycle: + bind: id: "999999999" diff --git a/acceptance/bundle/deploy/bind/invalid-resource-type/databricks.yml b/acceptance/bundle/deploy/bind/target-override/databricks.yml similarity index 50% rename from acceptance/bundle/deploy/bind/invalid-resource-type/databricks.yml rename to acceptance/bundle/deploy/bind/target-override/databricks.yml index 84e59cd7f52..f8012429493 100644 --- a/acceptance/bundle/deploy/bind/invalid-resource-type/databricks.yml +++ b/acceptance/bundle/deploy/bind/target-override/databricks.yml @@ -1,10 +1,10 @@ bundle: - name: test-bind-invalid-resource-type + name: test-bind-target-override resources: jobs: foo: - name: test-job + name: test-bind-job environments: - environment_key: default spec: @@ -13,11 +13,16 @@ resources: - task_key: my_task environment_key: default spark_python_task: - python_file: ./hello.py + python_file: /Workspace/test.py + lifecycle: + bind: + id: "999999999" targets: default: - bind: - foobar: + resources: + jobs: foo: - id: "123" + lifecycle: + bind: + id: "PLACEHOLDER_JOB_ID" diff --git a/acceptance/bundle/deploy/bind/delete-with-block/out.test.toml b/acceptance/bundle/deploy/bind/target-override/out.test.toml similarity index 100% rename from acceptance/bundle/deploy/bind/delete-with-block/out.test.toml rename to acceptance/bundle/deploy/bind/target-override/out.test.toml diff --git a/acceptance/bundle/deploy/bind/target-override/output.txt b/acceptance/bundle/deploy/bind/target-override/output.txt new file mode 100644 index 00000000000..01b1ecff408 --- /dev/null +++ b/acceptance/bundle/deploy/bind/target-override/output.txt @@ -0,0 +1,12 @@ + +>>> [CLI] bundle validate -o json +{ + "bind": { + "id": "[NEW_JOB_ID]" + } +} + +>>> [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 diff --git a/acceptance/bundle/deploy/bind/target-override/script b/acceptance/bundle/deploy/bind/target-override/script new file mode 100644 index 00000000000..f1bf38e0aba --- /dev/null +++ b/acceptance/bundle/deploy/bind/target-override/script @@ -0,0 +1,8 @@ +# The ID of the resource to adopt differs per target, so it is set through the usual +# target override of the resource; the root value is a placeholder here. +NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" + +trace $CLI bundle validate -o json | jq '.resources.jobs.foo.lifecycle' +trace $CLI bundle plan diff --git a/acceptance/bundle/deploy/bind/top-level-bind/databricks.yml b/acceptance/bundle/deploy/bind/top-level-bind/databricks.yml deleted file mode 100644 index 67833116972..00000000000 --- a/acceptance/bundle/deploy/bind/top-level-bind/databricks.yml +++ /dev/null @@ -1,21 +0,0 @@ -bundle: - name: test-bind-top-level - -bind: - jobs: - foo: - id: "123" - -resources: - jobs: - foo: - name: test-job - environments: - - environment_key: default - spec: - client: "1" - tasks: - - task_key: my_task - environment_key: default - spark_python_task: - python_file: ./hello.py diff --git a/acceptance/bundle/deploy/bind/top-level-bind/hello.py b/acceptance/bundle/deploy/bind/top-level-bind/hello.py deleted file mode 100644 index 11b15b1a458..00000000000 --- a/acceptance/bundle/deploy/bind/top-level-bind/hello.py +++ /dev/null @@ -1 +0,0 @@ -print("hello") diff --git a/acceptance/bundle/deploy/bind/top-level-bind/out.test.toml b/acceptance/bundle/deploy/bind/top-level-bind/out.test.toml deleted file mode 100644 index a927a5fbc06..00000000000 --- a/acceptance/bundle/deploy/bind/top-level-bind/out.test.toml +++ /dev/null @@ -1,2 +0,0 @@ -Cloud = false -EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/top-level-bind/output.txt b/acceptance/bundle/deploy/bind/top-level-bind/output.txt deleted file mode 100644 index 3a7e5f1c9bc..00000000000 --- a/acceptance/bundle/deploy/bind/top-level-bind/output.txt +++ /dev/null @@ -1,14 +0,0 @@ - ->>> musterr [CLI] bundle validate -Error: bind blocks are only supported under targets..bind, not at the root of the configuration - at bind - in databricks.yml:5:3 - - -Found 1 error - ->>> musterr [CLI] bundle plan -Error: bind blocks are only supported under targets..bind, not at the root of the configuration - at bind - in databricks.yml:5:3 - diff --git a/acceptance/bundle/deploy/bind/top-level-bind/script b/acceptance/bundle/deploy/bind/top-level-bind/script deleted file mode 100644 index 43a0bfe2e1e..00000000000 --- a/acceptance/bundle/deploy/bind/top-level-bind/script +++ /dev/null @@ -1,5 +0,0 @@ -# A bind block at the root level (outside a target) is a misplacement. It must be -# rejected: ignoring it as an unknown field would create the resource instead of -# binding the existing one. -trace musterr $CLI bundle validate -trace musterr $CLI bundle plan diff --git a/acceptance/bundle/deploy/bind/top-level-bind/test.toml b/acceptance/bundle/deploy/bind/top-level-bind/test.toml deleted file mode 100644 index 18b1a88417e..00000000000 --- a/acceptance/bundle/deploy/bind/top-level-bind/test.toml +++ /dev/null @@ -1 +0,0 @@ -Cloud = false diff --git a/acceptance/bundle/deploy/bind/variable-id/databricks.yml b/acceptance/bundle/deploy/bind/variable-id/databricks.yml index 50002aaa239..cbc7dd4413e 100644 --- a/acceptance/bundle/deploy/bind/variable-id/databricks.yml +++ b/acceptance/bundle/deploy/bind/variable-id/databricks.yml @@ -19,10 +19,6 @@ resources: environment_key: default spark_python_task: python_file: /Workspace/test.py - -targets: - default: - bind: - jobs: - foo: + lifecycle: + bind: id: ${var.job_id} diff --git a/bundle/bundle.go b/bundle/bundle.go index f8faaf85cf5..6762b38b91e 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -462,11 +462,3 @@ func (b *Bundle) IsImmutableFolder() bool { func (b *Bundle) IsSourceLinkedDeployment() bool { return b.SyncMode() == config.SyncModeInPlace } - -// BindConfig returns the selected target's bind blocks, or nil when no target is selected. -func (b *Bundle) BindConfig() config.Bind { - if b.Target == nil { - return nil - } - return b.Target.Bind -} diff --git a/bundle/config/bind.go b/bundle/config/bind.go deleted file mode 100644 index 1e8ec9aa9d4..00000000000 --- a/bundle/config/bind.go +++ /dev/null @@ -1,73 +0,0 @@ -package config - -import ( - "fmt" - "slices" - - "github.com/databricks/cli/libs/diag" -) - -// BindResource represents a single resource to bind with its workspace ID. -type BindResource struct { - ID string `json:"id"` -} - -// Bind defines existing workspace resources to bring under bundle management at deploy -// time. The outer map key is the resource type (e.g. "jobs", "pipelines"), the inner -// key is the resource name in the bundle config, and the value carries the workspace -// resource ID. Bind blocks are only valid for the direct deployment engine. -type Bind map[string]map[string]BindResource - -// ForEach calls fn for each bind entry in the configuration. Iteration order is -// stable (sorted by resource type, then resource name) so callers that emit user- -// visible diagnostics get deterministic output across runs. -func (b Bind) ForEach(fn func(resourceType, resourceName, bindID string)) { - resourceTypes := make([]string, 0, len(b)) - for resourceType := range b { - resourceTypes = append(resourceTypes, resourceType) - } - slices.Sort(resourceTypes) - for _, resourceType := range resourceTypes { - resources := b[resourceType] - names := make([]string, 0, len(resources)) - for name := range resources { - names = append(names, name) - } - slices.Sort(names) - for _, name := range names { - fn(resourceType, name, resources[name].ID) - } - } -} - -// IsEmpty returns true if no binds are defined. -func (b Bind) IsEmpty() bool { - for _, resources := range b { - if len(resources) > 0 { - return false - } - } - return true -} - -// Validate rejects bind blocks whose resource type is not a supported bundle -// resource. Only the resource types that can appear under the resources block are -// bindable; anything else (a typo, a child key like "jobs.permissions", etc.) is -// reported here rather than surfacing later as a confusing planning error. -func (b Bind) Validate() diag.Diagnostics { - var diags diag.Diagnostics - - supported := SupportedResources() - b.ForEach(func(resourceType, resourceName, bindID string) { - if _, ok := supported[resourceType]; ok { - return - } - diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("binding %q is not supported", resourceType), - Detail: fmt.Sprintf("%q is not a supported resource type; bind is only supported for resources that can be defined under the resources block.", resourceType), - }) - }) - - return diags -} diff --git a/bundle/config/bind_test.go b/bundle/config/bind_test.go deleted file mode 100644 index 88e2aacb9cf..00000000000 --- a/bundle/config/bind_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package config_test - -import ( - "testing" - - "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/libs/diag" - "github.com/stretchr/testify/assert" -) - -func TestBindIsEmpty(t *testing.T) { - cases := []struct { - name string - bind config.Bind - want bool - }{ - {"nil", nil, true}, - {"empty outer", config.Bind{}, true}, - {"empty inner", config.Bind{"jobs": {}}, true}, - {"populated", config.Bind{"jobs": {"foo": {ID: "1"}}}, false}, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - assert.Equal(t, c.want, c.bind.IsEmpty()) - }) - } -} - -func TestBindForEachIteratesInSortedOrder(t *testing.T) { - bind := config.Bind{ - "pipelines": {"baz": {ID: "3"}}, - "jobs": {"foo": {ID: "1"}, "bar": {ID: "2"}}, - } - type entry struct{ rt, rn, id string } - var got []entry - bind.ForEach(func(rt, rn, id string) { - got = append(got, entry{rt, rn, id}) - }) - // ForEach guarantees stable order so multi-error diagnostics are reproducible. - assert.Equal(t, []entry{ - {"jobs", "bar", "2"}, - {"jobs", "foo", "1"}, - {"pipelines", "baz", "3"}, - }, got) -} - -func TestBindValidate(t *testing.T) { - cases := []struct { - name string - bind config.Bind - wantError bool - }{ - {"supported resource", config.Bind{"jobs": {"foo": {ID: "1"}}}, false}, - {"another supported resource", config.Bind{"pipelines": {"foo": {ID: "1"}}}, false}, - {"permissions child", config.Bind{"jobs.permissions": {"foo": {ID: "1"}}}, true}, - {"grants child", config.Bind{"schemas.grants": {"foo": {ID: "1"}}}, true}, - {"unknown resource type", config.Bind{"my_permissions_jobs": {"foo": {ID: "1"}}}, true}, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - diags := c.bind.Validate() - if c.wantError { - assert.True(t, diags.HasError()) - assert.Equal(t, diag.Error, diags[0].Severity) - } else { - assert.False(t, diags.HasError()) - } - }) - } -} diff --git a/bundle/config/mutator/resolve_bind_references.go b/bundle/config/mutator/resolve_bind_references.go deleted file mode 100644 index b6b096c0e26..00000000000 --- a/bundle/config/mutator/resolve_bind_references.go +++ /dev/null @@ -1,65 +0,0 @@ -package mutator - -import ( - "context" - "slices" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/libs/diag" - "github.com/databricks/cli/libs/dyn" - "github.com/databricks/cli/libs/dyn/convert" - "github.com/databricks/cli/libs/dyn/dynvar" -) - -type resolveBindReferences struct{} - -// ResolveBindReferences resolves ${var.*}, ${bundle.*} and ${workspace.*} references -// in the selected target's bind IDs. SelectTarget keeps the target as a typed snapshot -// and drops the targets section from the config tree, so the regular variable -// resolution never sees bind blocks. -func ResolveBindReferences() bundle.Mutator { - return &resolveBindReferences{} -} - -func (m *resolveBindReferences) Name() string { - return "ResolveBindReferences" -} - -func (m *resolveBindReferences) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { - if b.Target == nil || b.Target.Bind.IsEmpty() { - return nil - } - - bind, err := convert.FromTyped(b.Target.Bind, dyn.NilValue) - if err != nil { - return diag.FromErr(err) - } - - prefixes := make([]dyn.Path, len(defaultPrefixes)) - for i, prefix := range defaultPrefixes { - prefixes[i] = dyn.MustPathFromString(prefix) - } - - // Same lookup root as resolveVariableReferences: fields present in the type but - // unset in config resolve to their empty value instead of failing. - normalized, _ := convert.Normalize(b.Config, b.Config.Value(), convert.IncludeMissingFields) - resolved, err := dynvar.Resolve(bind, func(path dyn.Path) (dyn.Value, error) { - path = rewriteVarShorthand(path) - if !slices.ContainsFunc(prefixes, path.HasPrefix) { - return dyn.InvalidValue, dynvar.ErrSkipResolution - } - return lookup(normalized, path, b) - }) - if err != nil { - return diag.FromErr(err) - } - - // A reference may resolve to a non-string (e.g. an integer job ID), so normalize - // against the field types before converting back. - resolved, diags := convert.Normalize(b.Target.Bind, resolved) - if diags.HasError() { - return diags - } - - return diags.Extend(diag.FromErr(convert.ToTyped(&b.Target.Bind, resolved))) -} diff --git a/bundle/config/mutator/resolve_bind_references_test.go b/bundle/config/mutator/resolve_bind_references_test.go deleted file mode 100644 index dbb0b7190df..00000000000 --- a/bundle/config/mutator/resolve_bind_references_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package mutator - -import ( - "testing" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/bundle/config/variable" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestResolveBindReferences(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Bundle: config.Bundle{Target: "dev"}, - Variables: map[string]*variable.Variable{ - "job_id": {Value: "123"}, - }, - }, - Target: &config.Target{ - Bind: config.Bind{ - "jobs": { - "foo": {ID: "${var.job_id}"}, - "bar": {ID: "${bundle.target}-456"}, - }, - }, - }, - } - - diags := bundle.Apply(t.Context(), b, ResolveBindReferences()) - require.NoError(t, diags.Error()) - assert.Equal(t, "123", b.Target.Bind["jobs"]["foo"].ID) - assert.Equal(t, "dev-456", b.Target.Bind["jobs"]["bar"].ID) -} - -func TestResolveBindReferencesIntegerValue(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Variables: map[string]*variable.Variable{ - "job_id": {Value: 123}, - }, - }, - Target: &config.Target{ - Bind: config.Bind{ - "jobs": {"foo": {ID: "${var.job_id}"}}, - }, - }, - } - - diags := bundle.Apply(t.Context(), b, ResolveBindReferences()) - require.NoError(t, diags.Error()) - assert.Equal(t, "123", b.Target.Bind["jobs"]["foo"].ID) -} - -func TestResolveBindReferencesUnknownVariable(t *testing.T) { - b := &bundle.Bundle{ - Target: &config.Target{ - Bind: config.Bind{ - "jobs": {"foo": {ID: "${var.missing}"}}, - }, - }, - } - - diags := bundle.Apply(t.Context(), b, ResolveBindReferences()) - assert.Error(t, diags.Error()) -} - -func TestResolveBindReferencesNoTarget(t *testing.T) { - b := &bundle.Bundle{} - diags := bundle.Apply(t.Context(), b, ResolveBindReferences()) - assert.NoError(t, diags.Error()) -} diff --git a/bundle/config/mutator/resolve_variable_references.go b/bundle/config/mutator/resolve_variable_references.go index cd9ba820cc8..42c22b61533 100644 --- a/bundle/config/mutator/resolve_variable_references.go +++ b/bundle/config/mutator/resolve_variable_references.go @@ -164,11 +164,15 @@ func (m *resolveVariableReferences) Apply(ctx context.Context, b *bundle.Bundle) prefixes[i] = dyn.MustPathFromString(prefix) } + // The path ${var.foo} is a shorthand for ${variables.foo.value}. + // We rewrite it here to make the resolution logic simpler. + varPath := dyn.NewPath(dyn.Key("var")) + var diags diag.Diagnostics maxRounds := 1 + m.extraRounds for round := range maxRounds { - hasUpdates, newDiags := m.resolveOnce(b, prefixes) + hasUpdates, newDiags := m.resolveOnce(b, prefixes, varPath) diags = diags.Extend(newDiags) @@ -197,27 +201,7 @@ func (m *resolveVariableReferences) Apply(ctx context.Context, b *bundle.Bundle) return diags } -// varPath is the ${var.foo} shorthand prefix for ${variables.foo.value}. -var varPath = dyn.NewPath(dyn.Key("var")) - -// rewriteVarShorthand rewrites the shorthand path ${var.foo} into ${variables.foo.value}, -// so that resolution only has to deal with the long form. -func rewriteVarShorthand(path dyn.Path) dyn.Path { - if !path.HasPrefix(varPath) { - return path - } - newPath := dyn.NewPath( - dyn.Key("variables"), - path[1], - dyn.Key("value"), - ) - if len(path) > 2 { - newPath = newPath.Append(path[2:]...) - } - return newPath -} - -func (m *resolveVariableReferences) resolveOnce(b *bundle.Bundle, prefixes []dyn.Path) (bool, diag.Diagnostics) { +func (m *resolveVariableReferences) resolveOnce(b *bundle.Bundle, prefixes []dyn.Path, varPath dyn.Path) (bool, diag.Diagnostics) { var diags diag.Diagnostics hasUpdates := false err := m.selectivelyMutate(b, func(root dyn.Value) (dyn.Value, error) { @@ -240,7 +224,20 @@ func (m *resolveVariableReferences) resolveOnce(b *bundle.Bundle, prefixes []dyn root, err := dyn.MapByPattern(root, m.pattern, func(p dyn.Path, v dyn.Value) (dyn.Value, error) { // Resolve variable references in all values. return dynvar.Resolve(v, func(path dyn.Path) (dyn.Value, error) { - path = rewriteVarShorthand(path) + // Rewrite the shorthand path ${var.foo} into ${variables.foo.value}. + if path.HasPrefix(varPath) { + newPath := dyn.NewPath( + dyn.Key("variables"), + path[1], + dyn.Key("value"), + ) + + if len(path) > 2 { + newPath = newPath.Append(path[2:]...) + } + + path = newPath + } // If the path starts with "artifacts", we need to add a metric to track if this reference is used. if path.HasPrefix(artifactPath) { diff --git a/bundle/config/mutator/validate_bind_resources.go b/bundle/config/mutator/validate_bind_resources.go deleted file mode 100644 index ae6d0b29395..00000000000 --- a/bundle/config/mutator/validate_bind_resources.go +++ /dev/null @@ -1,65 +0,0 @@ -package mutator - -import ( - "context" - "fmt" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/libs/diag" - "github.com/databricks/cli/libs/structs/structpath" -) - -type validateBindResources struct{} - -// ValidateBindResources validates the target's bind blocks against the config: the -// resource type must be bindable and the resource must be defined in the resources -// section. Both are pure config checks, so they run in the initialize phase and -// surface in "bundle validate", before any state is read or plan is attempted. -func ValidateBindResources() bundle.Mutator { - return &validateBindResources{} -} - -func (m *validateBindResources) Name() string { - return "ValidateBindResources" -} - -func (m *validateBindResources) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { - if b.Target == nil { - return nil - } - - diags := b.Target.Bind.Validate() - if diags.HasError() { - return diags - } - - // A bind block whose resource is not in config has nothing to bind. This also - // covers a resource removed from config while its bind block stayed: the state - // entry would otherwise plan as a delete of the very resource the block protects, - // so the user must drop the block to express that intent. - groups := b.Config.Resources.AllResources() - b.Target.Bind.ForEach(func(resourceType, resourceName, _ string) { - if hasResource(groups, resourceType, resourceName) { - return - } - key := "resources." + resourceType + "." + resourceName - diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("bind block references undefined resource %q; define it in the resources section or remove the bind block", key), - Paths: structpath.NewPathSlice("targets", b.Config.Bundle.Target, "bind", resourceType, resourceName), - }) - }) - - return diags -} - -func hasResource(groups []config.ResourceGroup, resourceType, resourceName string) bool { - for _, group := range groups { - if group.Description.PluralName == resourceType { - _, ok := group.Resources[resourceName] - return ok - } - } - return false -} diff --git a/bundle/config/mutator/validate_bind_resources_test.go b/bundle/config/mutator/validate_bind_resources_test.go deleted file mode 100644 index e58ebf1e8e4..00000000000 --- a/bundle/config/mutator/validate_bind_resources_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package mutator - -import ( - "testing" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/bundle/config/resources" - "github.com/stretchr/testify/assert" -) - -func TestValidateBindResourcesNoTarget(t *testing.T) { - b := &bundle.Bundle{} - diags := bundle.Apply(t.Context(), b, ValidateBindResources()) - assert.NoError(t, diags.Error()) -} - -func TestValidateBindResourcesAcceptsDefinedResource(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Jobs: map[string]*resources.Job{"foo": {}}, - }, - }, - Target: &config.Target{ - Bind: config.Bind{ - "jobs": {"foo": {ID: "1"}}, - }, - }, - } - diags := bundle.Apply(t.Context(), b, ValidateBindResources()) - assert.NoError(t, diags.Error()) -} - -func TestValidateBindResourcesRejectsUnsupportedType(t *testing.T) { - b := &bundle.Bundle{ - Target: &config.Target{ - Bind: config.Bind{ - "jobs.permissions": {"foo": {ID: "1"}}, - }, - }, - } - diags := bundle.Apply(t.Context(), b, ValidateBindResources()) - assert.Error(t, diags.Error()) - assert.Contains(t, diags[0].Summary, `binding "jobs.permissions" is not supported`) -} - -func TestValidateBindResourcesRejectsUndefinedResource(t *testing.T) { - b := &bundle.Bundle{ - Config: config.Root{ - Bundle: config.Bundle{Target: "dev"}, - Resources: config.Resources{ - Jobs: map[string]*resources.Job{"foo": {}}, - }, - }, - Target: &config.Target{ - Bind: config.Bind{ - "jobs": {"bar": {ID: "1"}}, - }, - }, - } - diags := bundle.Apply(t.Context(), b, ValidateBindResources()) - assert.Error(t, diags.Error()) - assert.Equal(t, `bind block references undefined resource "resources.jobs.bar"; define it in the resources section or remove the bind block`, diags[0].Summary) - assert.Equal(t, "targets.dev.bind.jobs.bar", diags[0].Paths[0].String()) -} diff --git a/bundle/config/resources/lifecycle.go b/bundle/config/resources/lifecycle.go index ec93ba5af06..90e849ba8cd 100644 --- a/bundle/config/resources/lifecycle.go +++ b/bundle/config/resources/lifecycle.go @@ -4,6 +4,7 @@ package resources type LifecycleConfig interface { HasPreventDestroy() bool GetDependsOn() []DependsOn + GetBind() *Bind } // Lifecycle contains base lifecycle settings supported by all resources. @@ -19,6 +20,9 @@ type Lifecycle struct { // Resources that must be deployed before this resource and deleted after it. DependsOn []DependsOn `json:"depends_on,omitempty"` + + // Existing workspace resource to bring under bundle management instead of creating one. + Bind *Bind `json:"bind,omitempty"` } // DependsOn is a single lifecycle.depends_on entry. @@ -27,6 +31,15 @@ type DependsOn struct { Resource string `json:"resource"` } +// Bind is the lifecycle.bind block: the deploy adopts the workspace resource with this +// ID into the deployment state instead of creating a new one. Nothing is recorded in +// state about the adoption; the block guards the resource against being recreated for +// as long as it is present in the configuration. +type Bind struct { + // ID of the existing workspace resource, as used by its API. + ID string `json:"id"` +} + // HasPreventDestroy returns true if prevent_destroy is set. func (l Lifecycle) HasPreventDestroy() bool { return l.PreventDestroy @@ -37,6 +50,11 @@ func (l Lifecycle) GetDependsOn() []DependsOn { return l.DependsOn } +// GetBind returns the lifecycle.bind block, or nil when the resource is not bound. +func (l Lifecycle) GetBind() *Bind { + return l.Bind +} + // LifecycleWithStarted contains lifecycle settings for resources that support lifecycle.started. // It is used by apps, clusters, and sql_warehouses. type LifecycleWithStarted struct { diff --git a/bundle/config/root.go b/bundle/config/root.go index 76293d715c8..a64b742630e 100644 --- a/bundle/config/root.go +++ b/bundle/config/root.go @@ -131,18 +131,6 @@ func LoadFromBytes(path string, raw []byte) (*Root, diag.Diagnostics) { return nil, diag.Errorf("failed to rewrite %s: %v", path, err) } - // A root-level bind block is a misplaced target-level block. Normalize would drop - // it as an unknown field with only a warning, and the deploy would then create the - // resources it names instead of binding the existing ones. - if bind, err := dyn.GetByPath(v, dyn.NewPath(dyn.Key("bind"))); err == nil { - return nil, diag.Diagnostics{{ - Severity: diag.Error, - Summary: "bind blocks are only supported under targets..bind, not at the root of the configuration", - Locations: bind.Locations(), - Paths: structpath.NewPathSlice("bind"), - }} - } - // Normalize dynamic configuration tree according to configuration type. v, diags := convert.Normalize(r, v) diff --git a/bundle/config/target.go b/bundle/config/target.go index a58e5191ee1..fae9c940b3f 100644 --- a/bundle/config/target.go +++ b/bundle/config/target.go @@ -69,11 +69,6 @@ type Target struct { Sync *Sync `json:"sync,omitempty"` Permissions []resources.Permission `json:"permissions,omitempty"` - - // Bind specifies existing workspace resources to bind into bundle management. - // Resources listed here will be bound to the bundle at deploy time. - // This field is only valid for the direct deployment engine. - Bind Bind `json:"bind,omitempty"` } const ( diff --git a/bundle/direct/bind.go b/bundle/direct/bind.go index 522b4b336e7..7fbf7ede662 100644 --- a/bundle/direct/bind.go +++ b/bundle/direct/bind.go @@ -124,7 +124,7 @@ func (b *DeploymentBundle) Bind(ctx context.Context, client *databricks.Workspac os.Remove(tmpStatePath) return nil, err } - plan, err := b.CalculatePlan(ctx, client, configRoot, nil) + plan, err := b.CalculatePlan(ctx, client, configRoot) if err != nil { os.Remove(tmpStatePath) return nil, err @@ -197,7 +197,7 @@ func (b *DeploymentBundle) Bind(ctx context.Context, client *databricks.Workspac os.Remove(tmpStatePath) return nil, err } - plan, err = b.CalculatePlan(ctx, client, configRoot, nil) + plan, err = b.CalculatePlan(ctx, client, configRoot) if _, ferr := b.StateDB.Finalize(ctx); ferr != nil { log.Warnf(ctx, "failed to finalize state: %v", ferr) } diff --git a/bundle/direct/bundle_plan.go b/bundle/direct/bundle_plan.go index cf5d39db8a0..2a1321c2fab 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -162,10 +162,9 @@ func (b *DeploymentBundle) StampDeploymentIdForFirstVersion(deploymentID string) // CalculatePlan computes the deployment plan by comparing local config against remote state. // StateDB must already be open for read before calling this function. // -// bindConfig carries the target's declarative bind blocks; resources listed there -// that are not yet in state are planned as Bind / BindAndUpdate instead of Create. -// Pass an empty Bind when there is nothing to bind (e.g. destroy). -func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks.WorkspaceClient, configRoot *config.Root, bindConfig config.Bind) (*deployplan.Plan, error) { +// Resources with lifecycle.bind that are not yet in state are planned as Bind / +// BindAndUpdate instead of Create; a nil configRoot (destroy) binds nothing. +func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks.WorkspaceClient, configRoot *config.Root) (*deployplan.Plan, error) { b.StateDB.AssertOpenedForRead() err := b.init(client) @@ -185,17 +184,12 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks b.Plan = plan - // Validate bind blocks against the full plan and state before walking the graph, - // then index them by resource key so the per-resource planning below can select - // Bind / BindAndUpdate for resources that are being bound for the first time. - bindIDByKey := map[string]string{} - if !bindConfig.IsEmpty() { - if !b.validateBindConfig(ctx, configRoot, bindConfig) { - return nil, errors.New("bind validation failed") - } - bindConfig.ForEach(func(resourceType, resourceName, bindID string) { - bindIDByKey["resources."+resourceType+"."+resourceName] = bindID - }) + // Validate bind blocks against the state before walking the graph; the per-resource + // planning below selects Bind / BindAndUpdate for resources being bound for the + // first time and guards the ones already bound. + bindIDByKey := lifecycleBindIDs(configRoot) + if len(bindIDByKey) > 0 && !b.validateBindConfig(ctx, configRoot, bindIDByKey) { + return nil, errors.New("bind validation failed") } g, err := makeGraph(plan) @@ -400,7 +394,7 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks // re-point the state at a new ID behind the user's back and leave the // block mismatched on every later plan, so report the deletion instead. if bound { - logdiag.LogError(ctx, fmt.Errorf("%s: bound resource with ID %q no longer exists in the workspace; remove the bind block to create a new resource", errorPrefix, dbentry.ID)) + logdiag.LogError(ctx, fmt.Errorf("%s: bound resource with ID %q no longer exists in the workspace; remove lifecycle.bind to create a new resource", errorPrefix, dbentry.ID)) return false } // Even if local action is "recreate" which is higher than "create", we should still pick "create" here diff --git a/bundle/direct/bundle_plan_bind.go b/bundle/direct/bundle_plan_bind.go index fe671472609..11f26c5f1b4 100644 --- a/bundle/direct/bundle_plan_bind.go +++ b/bundle/direct/bundle_plan_bind.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "maps" "slices" "strings" @@ -18,100 +19,104 @@ import ( "github.com/databricks/databricks-sdk-go/apierr" ) -// bindBlockPath returns the config path of a bind block, used to point +// lifecycleBindIDs collects lifecycle.bind IDs by resource key. A nil configRoot +// (destroy) has no bind blocks. +func lifecycleBindIDs(configRoot *config.Root) map[string]string { + result := map[string]string{} + if configRoot == nil { + return result + } + for _, group := range configRoot.Resources.AllResources() { + for name, resource := range group.Resources { + bind := resource.GetLifecycle().GetBind() + if bind == nil { + continue + } + result["resources."+group.Description.PluralName+"."+name] = bind.ID + } + } + return result +} + +// bindIDPath returns the config path of a resource's lifecycle.bind.id, used to point // diagnostics at the offending entry. -func bindBlockPath(targetName, resourceType, resourceName string) *structpath.PathNode { - return structpath.NewPath(nil, "targets", targetName, "bind", resourceType, resourceName) +func bindIDPath(resourceKey string) *structpath.PathNode { + resourceType, resourceName := splitResourceKey(resourceKey) + return structpath.NewPath(nil, "resources", resourceType, resourceName, "lifecycle", "bind", "id") } -// validateBindConfig logs diagnostics for invalid bind entries. Returns false if any -// errors were logged. -func (b *DeploymentBundle) validateBindConfig(ctx context.Context, configRoot *config.Root, bindConfig config.Bind) bool { - hasError := false - targetName := configRoot.Bundle.Target +func (b *DeploymentBundle) logBindError(ctx context.Context, configRoot *config.Root, resourceKey, summary string) { + path := bindIDPath(resourceKey) + logdiag.LogDiag(ctx, diag.Diagnostic{ + Severity: diag.Error, + Summary: summary, + Locations: configRoot.GetLocationsOf(path), + Paths: []*structpath.PathNode{path}, + }) +} - // Bind blocks referencing resources missing from config are rejected by - // mutator.ValidateBindResources before planning; the checks below need state. +// validateBindConfig logs diagnostics for lifecycle.bind entries that conflict with the +// state. Returns false if any errors were logged. +func (b *DeploymentBundle) validateBindConfig(ctx context.Context, configRoot *config.Root, bindIDByKey map[string]string) bool { + hasError := false + keys := slices.Sorted(maps.Keys(bindIDByKey)) - // A resource that is already in state cannot be re-bound to a different ID: the - // bind block is meant to import a resource for the first time. When the IDs match - // the bind is a no-op and the resource just deploys normally. - bindConfig.ForEach(func(resourceType, resourceName, bindID string) { - key := "resources." + resourceType + "." + resourceName + // A resource that is already in state cannot be re-bound to a different ID: + // lifecycle.bind adopts a resource for the first time. When the IDs match the bind + // is a no-op and the resource just deploys normally. + for _, key := range keys { + bindID := bindIDByKey[key] dbentry, hasEntry := b.StateDB.GetResourceEntry(key) if !hasEntry || dbentry.ID == "" || dbentry.ID == bindID { - return + continue } - bindPath := bindBlockPath(targetName, resourceType, resourceName) - logdiag.LogDiag(ctx, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("%s: resource already bound to ID %q, cannot bind as %q; remove the bind block or unbind the existing resource", key, dbentry.ID, bindID), - Locations: configRoot.GetLocationsOf(bindPath), - Paths: []*structpath.PathNode{bindPath}, - }) + b.logBindError(ctx, configRoot, key, fmt.Sprintf("%s: resource already bound to ID %q, cannot bind as %q; remove lifecycle.bind or unbind the existing resource", key, dbentry.ID, bindID)) hasError = true - }) + } - // Two bind blocks must not point at the same workspace ID. Otherwise both would - // be bound for the first time and create two state entries managing one remote - // resource; reject the collision instead of silently double-managing it. - bindKeysByID := map[string][]string{} - bindConfig.ForEach(func(resourceType, resourceName, bindID string) { - bindKey := "resources." + resourceType + "." + resourceName - bindKeysByID[bindID] = append(bindKeysByID[bindID], bindKey) - }) - bindConfig.ForEach(func(resourceType, resourceName, bindID string) { - keys := bindKeysByID[bindID] - if len(keys) < 2 { - return + // Two resources must not bind the same workspace ID. Otherwise both would be + // adopted and two state entries would manage one remote resource. IDs are only + // unique within a resource type (a job and an experiment can share a numeric ID), + // so only resources of the same type can collide. + keysByTypeAndID := map[string][]string{} + for _, key := range keys { + keysByTypeAndID[typeAndID(key, bindIDByKey[key])] = append(keysByTypeAndID[typeAndID(key, bindIDByKey[key])], key) + } + for _, key := range keys { + bindID := bindIDByKey[key] + others := slices.DeleteFunc(slices.Clone(keysByTypeAndID[typeAndID(key, bindID)]), func(k string) bool { return k == key }) + if len(others) == 0 { + continue } - bindKey := "resources." + resourceType + "." + resourceName - others := slices.DeleteFunc(slices.Clone(keys), func(k string) bool { return k == bindKey }) - bindPath := bindBlockPath(targetName, resourceType, resourceName) - logdiag.LogDiag(ctx, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("bind block for %q has the same ID %q as bind block(s) for %s; each workspace resource can only be bound once", bindKey, bindID, strings.Join(others, ", ")), - Locations: configRoot.GetLocationsOf(bindPath), - Paths: []*structpath.PathNode{bindPath}, - }) + b.logBindError(ctx, configRoot, key, fmt.Sprintf("%s has the same lifecycle.bind ID %q as %s; each workspace resource can only be bound once", key, bindID, strings.Join(others, ", "))) hasError = true - }) + } // A bind ID must not collide with another resource's ID in state. Such a collision // would let a bind silently redirect a delete/recreate/update_id at a resource the // user is also trying to import; reject it instead of choosing one over the other. - // IDs are only unique within a resource type (a job and an experiment can share a - // numeric ID), so only entries of the bind's own type can collide. - stateKeys := make([]string, 0, len(b.StateDB.Data.State)) - for stateKey := range b.StateDB.Data.State { - stateKeys = append(stateKeys, stateKey) - } - slices.Sort(stateKeys) - - bindConfig.ForEach(func(resourceType, resourceName, bindID string) { - bindKey := "resources." + resourceType + "." + resourceName + stateKeys := slices.Sorted(maps.Keys(b.StateDB.Data.State)) + for _, key := range keys { + bindID := bindIDByKey[key] for _, stateKey := range stateKeys { - stateEntry := b.StateDB.Data.State[stateKey] - if stateKey == bindKey || stateEntry.ID != bindID { - continue - } - if stateType, _ := splitResourceKey(stateKey); stateType != resourceType { + if stateKey == key || b.StateDB.Data.State[stateKey].ID != bindID || typeAndID(stateKey, bindID) != typeAndID(key, bindID) { continue } - bindPath := bindBlockPath(targetName, resourceType, resourceName) - logdiag.LogDiag(ctx, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("bind block for %q has the same ID %q as existing resource %q; remove the bind block or the conflicting resource", bindKey, bindID, stateKey), - Locations: configRoot.GetLocationsOf(bindPath), - Paths: []*structpath.PathNode{bindPath}, - }) + b.logBindError(ctx, configRoot, key, fmt.Sprintf("%s has the same lifecycle.bind ID %q as existing resource %q; remove lifecycle.bind or the conflicting resource", key, bindID, stateKey)) hasError = true } - }) + } return !hasError } +// typeAndID keys a bind by resource type and workspace ID, the scope within which IDs +// are unique. +func typeAndID(resourceKey, bindID string) string { + resourceType, _ := splitResourceKey(resourceKey) + return resourceType + ":" + bindID +} + // handleBindPlan reads the remote resource for a resource that is being bound for // the first time, computes the change set against the local config, and selects // either Bind (no changes) or BindAndUpdate (config drift can be applied). @@ -205,7 +210,7 @@ func (b *DeploymentBundle) handleBindPlan(ctx context.Context, resourceKey strin // buildBindConflictError formats a multi-line message that names the fields whose // changes forced an incompatible action (Recreate / UpdateWithID) and shows the two -// concrete resolutions: drop the offending fields, or drop the bind block. firstBind +// concrete resolutions: drop the offending fields, or drop lifecycle.bind. firstBind // tells a resource being adopted apart from one that is already bound in state. func buildBindConflictError(resourceKey string, action deployplan.ActionType, changes deployplan.Changes, firstBind bool) error { problematicFields := make([]string, 0, len(changes)) @@ -224,16 +229,16 @@ func buildBindConflictError(resourceKey string, action deployplan.ActionType, ch // action types are guarded by the caller. switch { //nolint:exhaustive case action == deployplan.Recreate && firstBind: - headline = "cannot bind " + resourceKey + ": cannot recreate resource with bind block" + headline = "cannot bind " + resourceKey + ": cannot recreate resource with lifecycle.bind" effect = "This would destroy and recreate the existing workspace resource, changing its ID." case action == deployplan.Recreate: - headline = "cannot plan " + resourceKey + ": bind block prevents recreating the bound resource" + headline = "cannot plan " + resourceKey + ": lifecycle.bind prevents recreating the bound resource" effect = "This would destroy and recreate the bound workspace resource, changing its ID." case action == deployplan.UpdateWithID && firstBind: - headline = "cannot bind " + resourceKey + ": cannot update resource ID with bind block" + headline = "cannot bind " + resourceKey + ": cannot update resource ID with lifecycle.bind" effect = "This would replace the existing workspace resource with a new ID." case action == deployplan.UpdateWithID: - headline = "cannot plan " + resourceKey + ": bind block prevents changing the ID of the bound resource" + headline = "cannot plan " + resourceKey + ": lifecycle.bind prevents changing the ID of the bound resource" effect = "This would replace the bound workspace resource with a new ID." } if firstBind { @@ -271,15 +276,15 @@ func buildBindConflictError(resourceKey string, action deployplan.ActionType, ch fmt.Fprintf(&msg, " # %s: ...\n", field) } msg.WriteString("\n") - msg.WriteString("2. Remove the bind block if you want to allow the resource to be recreated/updated:\n\n") - msg.WriteString(" targets:\n") - msg.WriteString(" :\n") - msg.WriteString(" # Remove the bind block:\n") - msg.WriteString(" # bind:\n") - fmt.Fprintf(&msg, " # %s:\n", resourceType) - fmt.Fprintf(&msg, " # %s:\n", resourceName) + msg.WriteString("2. Remove lifecycle.bind if you want to allow the resource to be recreated/updated:\n\n") + msg.WriteString(" resources:\n") + fmt.Fprintf(&msg, " %s:\n", resourceType) + fmt.Fprintf(&msg, " %s:\n", resourceName) + msg.WriteString(" lifecycle:\n") + msg.WriteString(" # Remove the bind block:\n") + msg.WriteString(" # bind:\n") // No trailing newline: error strings must not end with one. - msg.WriteString(" # id: ") + msg.WriteString(" # id: ") return errors.New(msg.String()) } diff --git a/bundle/internal/schema/annotations.yml b/bundle/internal/schema/annotations.yml index b0bff127ef2..181cbdbf858 100644 --- a/bundle/internal/schema/annotations.yml +++ b/bundle/internal/schema/annotations.yml @@ -629,6 +629,9 @@ resources: "description": |- Settings that control the deployment lifecycle of the resource, such as preventing it from being destroyed. "$fields": + "bind": + "description": |- + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. "depends_on": "description": |- Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -1117,6 +1120,9 @@ resources: "description": |- Settings that control the deployment lifecycle of the resource, such as preventing it from being destroyed and when the run re-fires. "$fields": + "bind": + "description": |- + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. "depends_on": "description": |- Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -1200,6 +1206,13 @@ resources: "description": |- Settings that control the deployment lifecycle of the resource, such as preventing it from being destroyed. "$fields": + "bind": + "description": |- + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + "$fields": + "id": + "description": |- + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. "depends_on": "description": |- Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -2263,13 +2276,6 @@ targets: "artifacts": "description": |- The artifacts to include in the target deployment. - "bind": - "description": |- - Existing workspace resources to bring under this target's management, keyed by resource type and by the resource key defined under `resources`. The first deploy adopts each resource (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block remains, deploys can neither delete nor recreate the resource. Removing the block lifts that protection, and `bundle destroy` deletes bound resources like any other; the deployment state does not record that a resource was bound. - "$fields": - "id": - "description": |- - The ID of the existing workspace resource to bind, as used by its API (for example a job ID, or the full name of a schema or volume). Variable references such as `${var.job_id}` are resolved. "bundle": "description": |- The bundle attributes when deploying to this target. diff --git a/bundle/internal/validation/generated/required_fields.go b/bundle/internal/validation/generated/required_fields.go index e1bb68848c0..3170efa9174 100644 --- a/bundle/internal/validation/generated/required_fields.go +++ b/bundle/internal/validation/generated/required_fields.go @@ -15,6 +15,7 @@ var RequiredFields = map[string][]string{ "resources.alerts.*.evaluation": {"comparison_operator", "source"}, "resources.alerts.*.evaluation.source": {"name"}, "resources.alerts.*.evaluation.threshold.column": {"name"}, + "resources.alerts.*.lifecycle.bind": {"id"}, "resources.alerts.*.lifecycle.depends_on[*]": {"resource"}, "resources.alerts.*.parameters[*]": {"name"}, "resources.alerts.*.permissions[*]": {"level"}, @@ -26,6 +27,7 @@ var RequiredFields = map[string][]string{ "resources.apps.*.default_git_source.git_repository": {"provider", "url"}, "resources.apps.*.git_repository": {"provider", "url"}, "resources.apps.*.git_source.git_repository": {"provider", "url"}, + "resources.apps.*.lifecycle.bind": {"id"}, "resources.apps.*.lifecycle.depends_on[*]": {"resource"}, "resources.apps.*.pending_deployment.git_source.git_repository": {"provider", "url"}, "resources.apps.*.permissions[*]": {"level"}, @@ -41,12 +43,14 @@ var RequiredFields = map[string][]string{ "resources.apps.*.telemetry_export_destinations[*].unity_catalog": {"logs_table", "metrics_table", "traces_table"}, "resources.catalogs.*": {"name"}, + "resources.catalogs.*.lifecycle.bind": {"id"}, "resources.catalogs.*.lifecycle.depends_on[*]": {"resource"}, "resources.catalogs.*.managed_encryption_settings.azure_encryption_settings": {"azure_tenant_id"}, "resources.cluster_policies.*.libraries[*].cran": {"package"}, "resources.cluster_policies.*.libraries[*].maven": {"coordinates"}, "resources.cluster_policies.*.libraries[*].pypi": {"package"}, + "resources.cluster_policies.*.lifecycle.bind": {"id"}, "resources.cluster_policies.*.lifecycle.depends_on[*]": {"resource"}, "resources.cluster_policies.*.permissions[*]": {"level"}, @@ -63,10 +67,12 @@ var RequiredFields = map[string][]string{ "resources.clusters.*.libraries[*].cran": {"package"}, "resources.clusters.*.libraries[*].maven": {"coordinates"}, "resources.clusters.*.libraries[*].pypi": {"package"}, + "resources.clusters.*.lifecycle.bind": {"id"}, "resources.clusters.*.lifecycle.depends_on[*]": {"resource"}, "resources.clusters.*.permissions[*]": {"level"}, "resources.clusters.*.workload_type": {"clients"}, + "resources.dashboards.*.lifecycle.bind": {"id"}, "resources.dashboards.*.lifecycle.depends_on[*]": {"resource"}, "resources.dashboards.*.permissions[*]": {"level"}, @@ -79,18 +85,22 @@ var RequiredFields = map[string][]string{ "resources.data_quality_monitors.*.lifecycle.depends_on[*]": {"resource"}, "resources.database_catalogs.*": {"database_instance_name", "database_name", "name"}, + "resources.database_catalogs.*.lifecycle.bind": {"id"}, "resources.database_catalogs.*.lifecycle.depends_on[*]": {"resource"}, "resources.database_instances.*": {"name"}, + "resources.database_instances.*.lifecycle.bind": {"id"}, "resources.database_instances.*.lifecycle.depends_on[*]": {"resource"}, "resources.database_instances.*.permissions[*]": {"level"}, "resources.experiments.*": {"name"}, + "resources.experiments.*.lifecycle.bind": {"id"}, "resources.experiments.*.lifecycle.depends_on[*]": {"resource"}, "resources.experiments.*.permissions[*]": {"level"}, "resources.experiments.*.trace_location.uc_trace_location": {"catalog", "schema"}, "resources.external_locations.*": {"credential_name", "name", "url"}, + "resources.external_locations.*.lifecycle.bind": {"id"}, "resources.external_locations.*.lifecycle.depends_on[*]": {"resource"}, "resources.features.*": {"full_name", "function", "source"}, @@ -119,6 +129,7 @@ var RequiredFields = map[string][]string{ "resources.features.*.function.custom_udf": {"function_path"}, "resources.features.*.function.custom_udf.input_bindings[*]": {"column", "parameter"}, "resources.features.*.function.extra_parameters[*]": {"key", "value"}, + "resources.features.*.lifecycle.bind": {"id"}, "resources.features.*.lifecycle.depends_on[*]": {"resource"}, "resources.features.*.source.delta_table_source": {"full_name"}, "resources.features.*.source.feature_view_source.feature_references[*]": {"feature"}, @@ -133,14 +144,17 @@ var RequiredFields = map[string][]string{ "resources.features.*.time_window.tumbling": {"window_duration"}, "resources.features.*.timeseries_column": {"name"}, + "resources.genie_spaces.*.lifecycle.bind": {"id"}, "resources.genie_spaces.*.lifecycle.depends_on[*]": {"resource"}, "resources.genie_spaces.*.permissions[*]": {"level"}, "resources.instance_pools.*": {"instance_pool_name", "node_type_id"}, + "resources.instance_pools.*.lifecycle.bind": {"id"}, "resources.instance_pools.*.lifecycle.depends_on[*]": {"resource"}, "resources.instance_pools.*.permissions[*]": {"level"}, "resources.job_runs.*": {"job_id"}, + "resources.job_runs.*.lifecycle.bind": {"id"}, "resources.job_runs.*.lifecycle.depends_on[*]": {"resource"}, "resources.job_runs.*.queue": {"enabled"}, @@ -163,6 +177,7 @@ var RequiredFields = map[string][]string{ "resources.jobs.*.job_clusters[*].new_cluster.init_scripts[*].volumes": {"destination"}, "resources.jobs.*.job_clusters[*].new_cluster.init_scripts[*].workspace": {"destination"}, "resources.jobs.*.job_clusters[*].new_cluster.workload_type": {"clients"}, + "resources.jobs.*.lifecycle.bind": {"id"}, "resources.jobs.*.lifecycle.depends_on[*]": {"resource"}, "resources.jobs.*.parameters[*]": {"default", "name"}, "resources.jobs.*.permissions[*]": {"level"}, @@ -307,6 +322,7 @@ var RequiredFields = map[string][]string{ "resources.mcp_services.*": {"parent", "mcp_service_id"}, "resources.mcp_services.*.config.rate_limits[*]": {"key", "renewal_period"}, "resources.mcp_services.*.config.source_connection": {"name"}, + "resources.mcp_services.*.lifecycle.bind": {"id"}, "resources.mcp_services.*.lifecycle.depends_on[*]": {"resource"}, "resources.model_provider_services.*": {"parent", "model_provider_service_id"}, @@ -329,6 +345,7 @@ var RequiredFields = map[string][]string{ "resources.model_provider_services.*.config.openai.direct.api_key.secret_reference": {"name"}, "resources.model_provider_services.*.config.rate_limits[*]": {"key", "renewal_period"}, "resources.model_provider_services.*.config.targets[*]": {"model"}, + "resources.model_provider_services.*.lifecycle.bind": {"id"}, "resources.model_provider_services.*.lifecycle.depends_on[*]": {"resource"}, "resources.model_services.*": {"parent", "model_service_id"}, @@ -344,6 +361,7 @@ var RequiredFields = map[string][]string{ "resources.model_services.*.config.routing.fallback.destinations[*].external_model_config.target": {"model"}, "resources.model_services.*.config.routing.fallback.destinations[*].pay_per_token_config": {"model"}, "resources.model_services.*.config.routing.fallback.destinations[*].provisioned_throughput_config": {"model_serving_endpoint"}, + "resources.model_services.*.lifecycle.bind": {"id"}, "resources.model_services.*.lifecycle.depends_on[*]": {"resource"}, "resources.model_serving_endpoints.*": {"name"}, @@ -357,12 +375,14 @@ var RequiredFields = map[string][]string{ "resources.model_serving_endpoints.*.config.served_entities[*].external_model.google_cloud_vertex_ai_config": {"project_id", "region"}, "resources.model_serving_endpoints.*.config.served_models[*]": {"model_name", "model_version", "scale_to_zero_enabled"}, "resources.model_serving_endpoints.*.config.traffic_config.routes[*]": {"traffic_percentage"}, + "resources.model_serving_endpoints.*.lifecycle.bind": {"id"}, "resources.model_serving_endpoints.*.lifecycle.depends_on[*]": {"resource"}, "resources.model_serving_endpoints.*.permissions[*]": {"level"}, "resources.model_serving_endpoints.*.rate_limits[*]": {"calls", "renewal_period"}, "resources.model_serving_endpoints.*.tags[*]": {"key"}, "resources.models.*": {"name"}, + "resources.models.*.lifecycle.bind": {"id"}, "resources.models.*.lifecycle.depends_on[*]": {"resource"}, "resources.models.*.permissions[*]": {"level"}, @@ -397,31 +417,39 @@ var RequiredFields = map[string][]string{ "resources.pipelines.*.ingestion_definition.objects[*].table.table_configuration.auto_full_refresh_policy": {"enabled"}, "resources.pipelines.*.ingestion_definition.table_configuration.auto_full_refresh_policy": {"enabled"}, "resources.pipelines.*.libraries[*].maven": {"coordinates"}, + "resources.pipelines.*.lifecycle.bind": {"id"}, "resources.pipelines.*.lifecycle.depends_on[*]": {"resource"}, "resources.pipelines.*.permissions[*]": {"level"}, "resources.pipelines.*.restart_window": {"start_hour"}, "resources.postgres_branches.*": {"branch_id", "parent"}, + "resources.postgres_branches.*.lifecycle.bind": {"id"}, "resources.postgres_branches.*.lifecycle.depends_on[*]": {"resource"}, "resources.postgres_catalogs.*": {"postgres_database", "catalog_id"}, + "resources.postgres_catalogs.*.lifecycle.bind": {"id"}, "resources.postgres_catalogs.*.lifecycle.depends_on[*]": {"resource"}, "resources.postgres_databases.*": {"role", "database_id", "parent"}, + "resources.postgres_databases.*.lifecycle.bind": {"id"}, "resources.postgres_databases.*.lifecycle.depends_on[*]": {"resource"}, "resources.postgres_endpoints.*": {"endpoint_type", "endpoint_id", "parent"}, "resources.postgres_endpoints.*.group": {"max", "min"}, + "resources.postgres_endpoints.*.lifecycle.bind": {"id"}, "resources.postgres_endpoints.*.lifecycle.depends_on[*]": {"resource"}, "resources.postgres_projects.*": {"project_id"}, + "resources.postgres_projects.*.lifecycle.bind": {"id"}, "resources.postgres_projects.*.lifecycle.depends_on[*]": {"resource"}, "resources.postgres_projects.*.permissions[*]": {"level"}, "resources.postgres_roles.*": {"role_id", "parent"}, + "resources.postgres_roles.*.lifecycle.bind": {"id"}, "resources.postgres_roles.*.lifecycle.depends_on[*]": {"resource"}, "resources.postgres_snapshot_schedules.*": {"branch"}, + "resources.postgres_snapshot_schedules.*.lifecycle.bind": {"id"}, "resources.postgres_snapshot_schedules.*.lifecycle.depends_on[*]": {"resource"}, "resources.postgres_snapshot_schedules.*.schedule[*]": {"retention"}, "resources.postgres_snapshot_schedules.*.schedule[*].monthly_schedule": {"day"}, @@ -429,44 +457,55 @@ var RequiredFields = map[string][]string{ "resources.postgres_synced_tables.*": {"synced_table_id"}, "resources.postgres_synced_tables.*.extra_columns[*]": {"column_name", "column_type"}, + "resources.postgres_synced_tables.*.lifecycle.bind": {"id"}, "resources.postgres_synced_tables.*.lifecycle.depends_on[*]": {"resource"}, "resources.postgres_synced_tables.*.type_overrides[*]": {"column_name", "pg_type"}, "resources.quality_monitors.*": {"assets_dir", "output_schema_name", "table_name"}, "resources.quality_monitors.*.custom_metrics[*]": {"definition", "input_columns", "name", "output_data_type", "type"}, "resources.quality_monitors.*.inference_log": {"granularities", "model_id_col", "prediction_col", "problem_type", "timestamp_col"}, + "resources.quality_monitors.*.lifecycle.bind": {"id"}, "resources.quality_monitors.*.lifecycle.depends_on[*]": {"resource"}, "resources.quality_monitors.*.schedule": {"quartz_cron_expression", "timezone_id"}, "resources.quality_monitors.*.time_series": {"granularities", "timestamp_col"}, + "resources.registered_models.*.lifecycle.bind": {"id"}, "resources.registered_models.*.lifecycle.depends_on[*]": {"resource"}, "resources.schemas.*": {"catalog_name", "name"}, + "resources.schemas.*.lifecycle.bind": {"id"}, "resources.schemas.*.lifecycle.depends_on[*]": {"resource"}, "resources.secret_scopes.*": {"name"}, "resources.secret_scopes.*.keyvault_metadata": {"dns_name", "resource_id"}, + "resources.secret_scopes.*.lifecycle.bind": {"id"}, "resources.secret_scopes.*.lifecycle.depends_on[*]": {"resource"}, "resources.secret_scopes.*.permissions[*]": {"level"}, "resources.secrets.*": {"catalog_name", "name", "schema_name", "value"}, + "resources.secrets.*.lifecycle.bind": {"id"}, "resources.secrets.*.lifecycle.depends_on[*]": {"resource"}, + "resources.sql_warehouses.*.lifecycle.bind": {"id"}, "resources.sql_warehouses.*.lifecycle.depends_on[*]": {"resource"}, "resources.sql_warehouses.*.permissions[*]": {"level"}, "resources.synced_database_tables.*": {"name"}, + "resources.synced_database_tables.*.lifecycle.bind": {"id"}, "resources.synced_database_tables.*.lifecycle.depends_on[*]": {"resource"}, "resources.synced_database_tables.*.spec.type_overrides[*]": {"column_name", "pg_type"}, "resources.vector_search_endpoints.*": {"endpoint_type", "name"}, + "resources.vector_search_endpoints.*.lifecycle.bind": {"id"}, "resources.vector_search_endpoints.*.lifecycle.depends_on[*]": {"resource"}, "resources.vector_search_endpoints.*.permissions[*]": {"level"}, "resources.vector_search_indexes.*": {"endpoint_name", "index_type", "name", "primary_key"}, + "resources.vector_search_indexes.*.lifecycle.bind": {"id"}, "resources.vector_search_indexes.*.lifecycle.depends_on[*]": {"resource"}, "resources.volumes.*": {"catalog_name", "name", "schema_name", "volume_type"}, + "resources.volumes.*.lifecycle.bind": {"id"}, "resources.volumes.*.lifecycle.depends_on[*]": {"resource"}, "scripts.*": {"content"}, diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index e5ea9bc8bee..18e7cf91ea7 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -146,12 +146,15 @@ func Unbind(ctx context.Context, b *bundle.Bundle, group, resourceKey string, en statemgmt.PushResourcesState(ctx, b) // Bind is state-less: nothing records that the resource was just unbound, so a - // bind block still in config re-adopts it on the next deploy. - if _, ok := b.BindConfig()[group][resourceKey]; ok { + // lifecycle.bind block still in config re-adopts it on the next deploy. + resource, _, err := b.Config.Resources.ResolveKey(group + "." + resourceKey) + if err == nil && resource.GetLifecycle().GetBind() != nil { + bindPath := structpath.NewPath(nil, "resources", group, resourceKey, "lifecycle", "bind") logdiag.LogDiag(ctx, diag.Diagnostic{ - Severity: diag.Warning, - Summary: fmt.Sprintf("the bind block for %s re-binds the resource on the next deploy; remove it to keep the resource unbound", fullResourceKey), - Paths: structpath.NewPathSlice("targets", b.Config.Bundle.Target, "bind", group, resourceKey), + Severity: diag.Warning, + Summary: fullResourceKey + ".lifecycle.bind re-binds the resource on the next deploy; remove it to keep the resource unbound", + Locations: b.Config.GetLocationsOf(bindPath), + Paths: []*structpath.PathNode{bindPath}, }) } } diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index 7e1337796c2..ef7b2a816ea 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -358,7 +358,7 @@ func Deploy(ctx context.Context, b *bundle.Bundle, outputHandler sync.OutputHand } func RunPlan(ctx context.Context, b *bundle.Bundle) *deployplan.Plan { - plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config, b.BindConfig()) + plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config) if err != nil { logdiag.LogError(ctx, err) return nil diff --git a/bundle/phases/destroy.go b/bundle/phases/destroy.go index 3cfa7a888c1..8e0e87687e6 100644 --- a/bundle/phases/destroy.go +++ b/bundle/phases/destroy.go @@ -322,12 +322,10 @@ func Destroy(ctx context.Context, b *bundle.Bundle) { bundle.ApplyContext(ctx, b, lock.Release(lock.GoalDestroy)) }() - // bundle destroy is allowed even when bind blocks reference resources in - // state: naming destroy explicitly captures the user's intent to tear the - // deployment down. The bind block only guards against accidental deletion - // during a normal deploy (a bound resource dropped from config or a change - // that would recreate it), not against a deliberate destroy. - plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), nil, nil) + // Planned without config, so lifecycle.bind plays no role: naming destroy explicitly + // captures the intent to tear the deployment down, bound resources included. The + // block only guards against a recreate during a normal deploy. + plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), nil) if err != nil { logdiag.LogError(ctx, err) return diff --git a/bundle/phases/initialize.go b/bundle/phases/initialize.go index 3e8e0d56de9..c855935598f 100644 --- a/bundle/phases/initialize.go +++ b/bundle/phases/initialize.go @@ -255,14 +255,6 @@ func Initialize(ctx context.Context, b *bundle.Bundle) { // Validates that when using a shared workspace path, appropriate permissions are configured permissions.ValidateSharedRootPermissions(), - // Reads (dynamic): bundle, workspace, variables - // Updates (typed): b.Target.Bind[*][*].ID (resolves ${var.*} and similar references in bind IDs) - mutator.ResolveBindReferences(), - - // Reads (typed): b.Target.Bind, b.Config.Resources - // Reports validation errors when a bind block names an unsupported resource type or a resource that is not defined. - mutator.ValidateBindResources(), - // Annotate resources with "deployment" metadata. // // We don't include this step into initializeResources because these mutators set fields that are diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index 8c31f667cbc..1951cbe16cd 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -350,6 +350,27 @@ } ] }, + "resources.Bind": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "description": "The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false, + "required": [ + "id" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "resources.Catalog": { "oneOf": [ { @@ -1675,6 +1696,10 @@ { "type": "object", "properties": { + "bind": { + "description": "Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption.", + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.Bind" + }, "depends_on": { "description": "Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering.", "$ref": "#/$defs/slice/github.com/databricks/cli/bundle/config/resources.DependsOn" @@ -1723,6 +1748,10 @@ { "type": "object", "properties": { + "bind": { + "description": "Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption.", + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.Bind" + }, "depends_on": { "description": "Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering.", "$ref": "#/$defs/slice/github.com/databricks/cli/bundle/config/resources.DependsOn" @@ -1745,6 +1774,10 @@ { "type": "object", "properties": { + "bind": { + "description": "Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption.", + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.Bind" + }, "depends_on": { "description": "Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering.", "$ref": "#/$defs/slice/github.com/databricks/cli/bundle/config/resources.DependsOn" @@ -3949,27 +3982,6 @@ "config.ArtifactType": { "type": "string" }, - "config.BindResource": { - "oneOf": [ - { - "type": "object", - "properties": { - "id": { - "description": "The ID of the existing workspace resource to bind, as used by its API (for example a job ID, or the full name of a schema or volume). Variable references such as `${var.job_id}` are resolved.", - "$ref": "#/$defs/string" - } - }, - "additionalProperties": false, - "required": [ - "id" - ] - }, - { - "type": "string", - "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" - } - ] - }, "config.Bundle": { "oneOf": [ { @@ -4511,10 +4523,6 @@ "description": "The artifacts to include in the target deployment.", "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config.Artifact" }, - "bind": { - "description": "Existing workspace resources to bring under this target's management, keyed by resource type and by the resource key defined under `resources`. The first deploy adopts each resource (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block remains, deploys can neither delete nor recreate the resource. Removing the block lifts that protection, and `bundle destroy` deletes bound resources like any other; the deployment state does not record that a resource was bound.", - "$ref": "#/$defs/map/map/github.com/databricks/cli/bundle/config.BindResource" - }, "bundle": { "description": "The bundle attributes when deploying to this target.", "$ref": "#/$defs/github.com/databricks/cli/bundle/config.Bundle" @@ -22125,20 +22133,6 @@ } ] }, - "config.BindResource": { - "oneOf": [ - { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/github.com/databricks/cli/bundle/config.BindResource" - } - }, - { - "type": "string", - "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" - } - ] - }, "config.Command": { "oneOf": [ { @@ -22185,30 +22179,6 @@ } } }, - "map": { - "github.com": { - "databricks": { - "cli": { - "bundle": { - "config.BindResource": { - "oneOf": [ - { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config.BindResource" - } - }, - { - "type": "string", - "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" - } - ] - } - } - } - } - } - }, "string": { "oneOf": [ { diff --git a/bundle/statemgmt/direct_migration.go b/bundle/statemgmt/direct_migration.go index 2668ecb0835..4fefd8504ac 100644 --- a/bundle/statemgmt/direct_migration.go +++ b/bundle/statemgmt/direct_migration.go @@ -200,8 +200,7 @@ func checkPlanOnTempState(ctx context.Context, b *bundle.Bundle, tempStatePath s return nil, fmt.Errorf("opening migrated state for plan check: %w", err) } - // Pass the bind blocks so the check plans what the deploy will plan. - return planBundle.CalculatePlan(planCtx, b.WorkspaceClient(ctx), cfg, b.BindConfig()) + return planBundle.CalculatePlan(planCtx, b.WorkspaceClient(ctx), cfg) } // recreatedResources returns the sorted keys of resources the plan would recreate diff --git a/cmd/bundle/config_remote_sync.go b/cmd/bundle/config_remote_sync.go index fc100a59fa4..67f74e926bf 100644 --- a/cmd/bundle/config_remote_sync.go +++ b/cmd/bundle/config_remote_sync.go @@ -90,9 +90,7 @@ Examples: // ProcessBundleRet has already opened the direct state for reading. deployBundle := &b.DeploymentBundle - // Without the target's bind blocks: remote-sync reconciles resources that - // are already in state, and adopting new ones is a deploy-time decision. - plan, err := deployBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config, nil) + plan, err := deployBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config) if err != nil { stats.ErrorCategory = protos.BundleConfigRemoteSyncErrorCategoryDetectChangesFailed return fmt.Errorf("failed to detect changes: %w", err) From 817f4f145c88a62fd7f10e337173709c80f5d8eb Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 14:36:44 +0000 Subject: [PATCH 13/20] Link the new pull request from the bind changelog fragment Co-authored-by: Isaac --- .nextchanges/bundles/4630.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nextchanges/bundles/4630.md b/.nextchanges/bundles/4630.md index 142b2b74d5d..73d60dfceb6 100644 --- a/.nextchanges/bundles/4630.md +++ b/.nextchanges/bundles/4630.md @@ -1 +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)). +* 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)) From e243463290105bcc5c09bcc9dcc7c05717df3337 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 15:20:21 +0000 Subject: [PATCH 14/20] Fix CI for lifecycle.bind: generated models, input subset test, cloud test isolation - Regenerate the PyDABs models: Lifecycle gains bind (validate-generated). - List lifecycle.bind as a non-state field for the resources whose state keeps a lifecycle sub-struct (apps, clusters, sql_warehouses, job_runs), like prevent_destroy and depends_on (TestInputSubset). - Give every deploying bind test a per-run bundle name. The deployment-history variants and the OS runners run concurrently against one workspace, so a fixed name shared the root path and failed with 'Failed to acquire deployment lock' or with the other variant's state. - bind-and-resize: create the cluster with the instance pool only; the cloud API rejects node_type_id together with instance_pool_id. - dev-mode: assert only the planned new values; a real workspace reports schedule.pause_status: UNPAUSED where the test server omits it. Co-authored-by: Isaac --- .../bundle/deploy/bind/basic/databricks.yml | 2 +- .../bundle/deploy/bind/basic/output.txt | 8 ++-- acceptance/bundle/deploy/bind/basic/script | 4 ++ .../bind/bind-and-resize/databricks.yml.tmpl | 1 - .../bundle/deploy/bind/bind-and-resize/script | 5 ++- .../bind/bind-and-update/databricks.yml | 2 +- .../deploy/bind/bind-and-update/output.txt | 6 +-- .../bundle/deploy/bind/bind-and-update/script | 2 + .../deploy/bind/block-removed/databricks.yml | 2 +- .../bind/block-removed/databricks_unbound.yml | 2 +- .../deploy/bind/block-removed/output.txt | 4 +- .../bundle/deploy/bind/block-removed/script | 3 ++ .../delete-and-bind-conflict/databricks.yml | 5 +-- .../databricks_conflict.yml | 2 +- .../bind/delete-and-bind-conflict/output.txt | 4 +- .../bind/delete-and-bind-conflict/script | 3 ++ .../bind/destroy-allowed/databricks.yml | 2 +- .../deploy/bind/destroy-allowed/output.txt | 4 +- .../bundle/deploy/bind/destroy-allowed/script | 2 + .../bundle/deploy/bind/dev-mode/output.txt | 4 -- acceptance/bundle/deploy/bind/dev-mode/script | 4 +- .../bind/duplicate-bind-id/databricks.yml | 5 +-- .../databricks_with_bind.yml | 2 +- .../deploy/bind/duplicate-bind-id/output.txt | 4 +- .../deploy/bind/duplicate-bind-id/script | 3 ++ .../deploy/bind/gone-in-state/databricks.yml | 2 +- .../deploy/bind/gone-in-state/output.txt | 6 +-- .../bundle/deploy/bind/gone-in-state/script | 2 + .../databricks.yml | 5 +-- .../databricks_with_bind.yml | 2 +- .../id-collision-different-type/output.txt | 4 +- .../bind/id-collision-different-type/script | 3 ++ .../deploy/bind/pure-bind/databricks.yml | 2 +- .../bundle/deploy/bind/pure-bind/output.txt | 6 +-- .../bundle/deploy/bind/pure-bind/script | 2 + .../bind/rebind-different-id/databricks.yml | 5 +-- .../databricks_with_bind.yml | 2 +- .../bind/rebind-different-id/output.txt | 4 +- .../deploy/bind/rebind-different-id/script | 3 ++ .../recreate-blocked-in-state/databricks.yml | 2 +- .../bind/recreate-blocked-in-state/output.txt | 14 ++++++- .../bind/recreate-blocked-in-state/script | 6 +++ .../deploy/bind/ref-to-bound/databricks.yml | 2 +- .../deploy/bind/ref-to-bound/output.txt | 4 +- .../bundle/deploy/bind/ref-to-bound/script | 2 + .../bind/removed-from-config/databricks.yml | 2 +- .../removed-from-config/databricks_nores.yml | 2 +- .../bind/removed-from-config/output.txt | 6 +-- .../deploy/bind/removed-from-config/script | 3 ++ .../deploy/bind/variable-id/databricks.yml | 2 +- .../bundle/deploy/bind/variable-id/output.txt | 4 +- .../bundle/deploy/bind/variable-id/script | 2 + bundle/direct/dresources/type_test.go | 4 ++ python/databricks/bundles/alerts/__init__.py | 4 ++ .../bundles/alerts/_models/alert.py | 5 +-- .../databricks/bundles/alerts/_models/bind.py | 40 +++++++++++++++++++ .../bundles/alerts/_models/lifecycle.py | 11 +++++ python/databricks/bundles/apps/__init__.py | 4 ++ .../databricks/bundles/apps/_models/bind.py | 40 +++++++++++++++++++ .../apps/_models/lifecycle_with_started.py | 16 ++++++-- .../databricks/bundles/catalogs/__init__.py | 4 ++ .../bundles/catalogs/_models/bind.py | 40 +++++++++++++++++++ .../bundles/catalogs/_models/catalog.py | 5 +-- .../bundles/catalogs/_models/lifecycle.py | 16 +++++++- .../bundles/cluster_policies/__init__.py | 4 ++ .../bundles/cluster_policies/_models/bind.py | 40 +++++++++++++++++++ .../cluster_policies/_models/lifecycle.py | 11 +++++ .../databricks/bundles/clusters/__init__.py | 4 ++ .../bundles/clusters/_models/bind.py | 40 +++++++++++++++++++ .../_models/lifecycle_with_started.py | 16 ++++++-- .../databricks/bundles/dashboards/__init__.py | 4 ++ .../bundles/dashboards/_models/bind.py | 40 +++++++++++++++++++ .../bundles/dashboards/_models/dashboard.py | 5 +-- .../bundles/dashboards/_models/lifecycle.py | 11 +++++ .../bundles/database_catalogs/__init__.py | 4 ++ .../bundles/database_catalogs/_models/bind.py | 40 +++++++++++++++++++ .../database_catalogs/_models/lifecycle.py | 11 +++++ .../bundles/database_instances/__init__.py | 4 ++ .../database_instances/_models/bind.py | 40 +++++++++++++++++++ .../database_instances/_models/lifecycle.py | 11 +++++ .../bundles/experiments/__init__.py | 4 ++ .../bundles/experiments/_models/bind.py | 40 +++++++++++++++++++ .../bundles/experiments/_models/lifecycle.py | 16 ++++++-- .../experiments/_models/mlflow_experiment.py | 5 ++- .../bundles/external_locations/__init__.py | 4 ++ .../external_locations/_models/bind.py | 40 +++++++++++++++++++ .../external_locations/_models/lifecycle.py | 11 +++++ .../databricks/bundles/features/__init__.py | 4 ++ .../bundles/features/_models/bind.py | 40 +++++++++++++++++++ .../bundles/features/_models/feature.py | 5 ++- .../bundles/features/_models/lifecycle.py | 11 +++++ .../bundles/genie_spaces/__init__.py | 4 ++ .../bundles/genie_spaces/_models/bind.py | 40 +++++++++++++++++++ .../genie_spaces/_models/genie_space.py | 5 +-- .../bundles/genie_spaces/_models/lifecycle.py | 11 +++++ .../bundles/instance_pools/__init__.py | 4 ++ .../bundles/instance_pools/_models/bind.py | 40 +++++++++++++++++++ .../instance_pools/_models/lifecycle.py | 11 +++++ .../databricks/bundles/job_runs/__init__.py | 4 ++ .../bundles/job_runs/_models/bind.py | 40 +++++++++++++++++++ .../job_runs/_models/job_run_lifecycle.py | 11 +++++ python/databricks/bundles/jobs/__init__.py | 4 ++ .../databricks/bundles/jobs/_models/bind.py | 40 +++++++++++++++++++ .../bundles/jobs/_models/lifecycle.py | 16 ++++++-- .../bundles/mcp_services/__init__.py | 4 ++ .../bundles/mcp_services/_models/bind.py | 40 +++++++++++++++++++ .../bundles/mcp_services/_models/lifecycle.py | 11 +++++ .../mcp_services/_models/mcp_service.py | 5 ++- .../model_provider_services/__init__.py | 8 ++++ .../model_provider_services/_models/bind.py | 40 +++++++++++++++++++ .../_models/lifecycle.py | 14 +++++++ .../bundles/model_services/__init__.py | 4 ++ .../bundles/model_services/_models/bind.py | 40 +++++++++++++++++++ .../model_services/_models/lifecycle.py | 11 +++++ .../model_serving_endpoints/__init__.py | 8 ++++ .../model_serving_endpoints/_models/bind.py | 40 +++++++++++++++++++ .../_models/lifecycle.py | 11 +++++ python/databricks/bundles/models/__init__.py | 4 ++ .../databricks/bundles/models/_models/bind.py | 40 +++++++++++++++++++ .../bundles/models/_models/lifecycle.py | 16 ++++++-- .../bundles/models/_models/mlflow_model.py | 5 +-- .../databricks/bundles/pipelines/__init__.py | 4 ++ .../bundles/pipelines/_models/bind.py | 40 +++++++++++++++++++ .../bundles/pipelines/_models/lifecycle.py | 16 ++++++-- .../bundles/pipelines/_models/pipeline.py | 5 +-- .../bundles/quality_monitors/__init__.py | 4 ++ .../bundles/quality_monitors/_models/bind.py | 40 +++++++++++++++++++ .../quality_monitors/_models/lifecycle.py | 11 +++++ .../bundles/registered_models/__init__.py | 4 ++ .../bundles/registered_models/_models/bind.py | 40 +++++++++++++++++++ .../registered_models/_models/lifecycle.py | 11 +++++ python/databricks/bundles/schemas/__init__.py | 4 ++ .../bundles/schemas/_models/bind.py | 40 +++++++++++++++++++ .../bundles/schemas/_models/lifecycle.py | 16 ++++++-- .../bundles/schemas/_models/schema.py | 5 +-- .../bundles/secret_scopes/__init__.py | 4 ++ .../bundles/secret_scopes/_models/bind.py | 40 +++++++++++++++++++ .../secret_scopes/_models/lifecycle.py | 11 +++++ .../secret_scopes/_models/secret_scope.py | 5 +-- python/databricks/bundles/secrets/__init__.py | 4 ++ .../bundles/secrets/_models/bind.py | 40 +++++++++++++++++++ .../bundles/secrets/_models/lifecycle.py | 16 ++++++-- .../bundles/secrets/_models/secret.py | 5 +-- .../bundles/sql_warehouses/__init__.py | 4 ++ .../bundles/sql_warehouses/_models/bind.py | 40 +++++++++++++++++++ .../_models/lifecycle_with_started.py | 11 +++++ .../synced_database_tables/__init__.py | 8 ++++ .../synced_database_tables/_models/bind.py | 40 +++++++++++++++++++ .../_models/lifecycle.py | 14 +++++++ .../vector_search_endpoints/__init__.py | 8 ++++ .../vector_search_endpoints/_models/bind.py | 40 +++++++++++++++++++ .../_models/lifecycle.py | 11 +++++ .../bundles/vector_search_indexes/__init__.py | 8 ++++ .../vector_search_indexes/_models/bind.py | 40 +++++++++++++++++++ .../_models/lifecycle.py | 14 +++++++ python/databricks/bundles/volumes/__init__.py | 4 ++ .../bundles/volumes/_models/bind.py | 40 +++++++++++++++++++ .../bundles/volumes/_models/lifecycle.py | 16 ++++++-- .../bundles/volumes/_models/volume.py | 5 +-- 159 files changed, 1884 insertions(+), 153 deletions(-) create mode 100644 python/databricks/bundles/alerts/_models/bind.py create mode 100644 python/databricks/bundles/apps/_models/bind.py create mode 100644 python/databricks/bundles/catalogs/_models/bind.py create mode 100644 python/databricks/bundles/cluster_policies/_models/bind.py create mode 100644 python/databricks/bundles/clusters/_models/bind.py create mode 100644 python/databricks/bundles/dashboards/_models/bind.py create mode 100644 python/databricks/bundles/database_catalogs/_models/bind.py create mode 100644 python/databricks/bundles/database_instances/_models/bind.py create mode 100644 python/databricks/bundles/experiments/_models/bind.py create mode 100644 python/databricks/bundles/external_locations/_models/bind.py create mode 100644 python/databricks/bundles/features/_models/bind.py create mode 100644 python/databricks/bundles/genie_spaces/_models/bind.py create mode 100644 python/databricks/bundles/instance_pools/_models/bind.py create mode 100644 python/databricks/bundles/job_runs/_models/bind.py create mode 100644 python/databricks/bundles/jobs/_models/bind.py create mode 100644 python/databricks/bundles/mcp_services/_models/bind.py create mode 100644 python/databricks/bundles/model_provider_services/_models/bind.py create mode 100644 python/databricks/bundles/model_services/_models/bind.py create mode 100644 python/databricks/bundles/model_serving_endpoints/_models/bind.py create mode 100644 python/databricks/bundles/models/_models/bind.py create mode 100644 python/databricks/bundles/pipelines/_models/bind.py create mode 100644 python/databricks/bundles/quality_monitors/_models/bind.py create mode 100644 python/databricks/bundles/registered_models/_models/bind.py create mode 100644 python/databricks/bundles/schemas/_models/bind.py create mode 100644 python/databricks/bundles/secret_scopes/_models/bind.py create mode 100644 python/databricks/bundles/secrets/_models/bind.py create mode 100644 python/databricks/bundles/sql_warehouses/_models/bind.py create mode 100644 python/databricks/bundles/synced_database_tables/_models/bind.py create mode 100644 python/databricks/bundles/vector_search_endpoints/_models/bind.py create mode 100644 python/databricks/bundles/vector_search_indexes/_models/bind.py create mode 100644 python/databricks/bundles/volumes/_models/bind.py diff --git a/acceptance/bundle/deploy/bind/basic/databricks.yml b/acceptance/bundle/deploy/bind/basic/databricks.yml index 3891ee43639..5a33ba1ced8 100644 --- a/acceptance/bundle/deploy/bind/basic/databricks.yml +++ b/acceptance/bundle/deploy/bind/basic/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-basic + name: test-bind-basic-UNIQUE_NAME resources: jobs: diff --git a/acceptance/bundle/deploy/bind/basic/output.txt b/acceptance/bundle/deploy/bind/basic/output.txt index c8ad40f18b0..4bbb8bff679 100644 --- a/acceptance/bundle/deploy/bind/basic/output.txt +++ b/acceptance/bundle/deploy/bind/basic/output.txt @@ -5,7 +5,7 @@ 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/default/files... +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 @@ -25,7 +25,7 @@ Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged "state": { "deployment": { "kind": "BUNDLE", - "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bind-basic/default/state/metadata.json" + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bind-basic-[UNIQUE_NAME]/default/state/metadata.json" }, "edit_mode": "UI_LOCKED", "environments": [ @@ -46,7 +46,7 @@ Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged { "environment_key": "default", "spark_python_task": { - "python_file": "/Workspace/Users/[USERNAME]/.bundle/test-bind-basic/default/files/hello.py" + "python_file": "/Workspace/Users/[USERNAME]/.bundle/test-bind-basic-[UNIQUE_NAME]/default/files/hello.py" }, "task_key": "my_task" } @@ -60,6 +60,6 @@ Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged 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/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-basic-[UNIQUE_NAME]/default Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/basic/script b/acceptance/bundle/deploy/bind/basic/script index fc56b7caad2..557be0c9b33 100644 --- a/acceptance/bundle/deploy/bind/basic/script +++ b/acceptance/bundle/deploy/bind/basic/script @@ -1,3 +1,7 @@ +# 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 diff --git a/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl b/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl index 8118cc18b4c..6332e9f172d 100644 --- a/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl +++ b/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl @@ -9,7 +9,6 @@ resources: test_cluster: cluster_name: test-cluster-$UNIQUE_NAME spark_version: $DEFAULT_SPARK_VERSION - node_type_id: $NODE_TYPE_ID instance_pool_id: $TEST_INSTANCE_POOL_ID num_workers: 3 lifecycle: diff --git a/acceptance/bundle/deploy/bind/bind-and-resize/script b/acceptance/bundle/deploy/bind/bind-and-resize/script index 8b77a43b9db..271eaf9d447 100644 --- a/acceptance/bundle/deploy/bind/bind-and-resize/script +++ b/acceptance/bundle/deploy/bind/bind-and-resize/script @@ -1,8 +1,9 @@ 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. -CLUSTER_ID=$($CLI clusters create --json "{\"cluster_name\": \"test-cluster-$UNIQUE_NAME\", \"spark_version\": \"$DEFAULT_SPARK_VERSION\", \"node_type_id\": \"$NODE_TYPE_ID\", \"instance_pool_id\": \"$TEST_INSTANCE_POOL_ID\", \"num_workers\": 2, \"autotermination_minutes\": 60}" | jq -r .cluster_id) +# 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" diff --git a/acceptance/bundle/deploy/bind/bind-and-update/databricks.yml b/acceptance/bundle/deploy/bind/bind-and-update/databricks.yml index b6b2980dd85..0c5a6a9fa9e 100644 --- a/acceptance/bundle/deploy/bind/bind-and-update/databricks.yml +++ b/acceptance/bundle/deploy/bind/bind-and-update/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-update + name: test-bind-update-UNIQUE_NAME resources: jobs: diff --git a/acceptance/bundle/deploy/bind/bind-and-update/output.txt b/acceptance/bundle/deploy/bind/bind-and-update/output.txt index 87bf7c30d14..202baea71d5 100644 --- a/acceptance/bundle/deploy/bind/bind-and-update/output.txt +++ b/acceptance/bundle/deploy/bind/bind-and-update/output.txt @@ -5,7 +5,7 @@ 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/default/files... +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 @@ -19,7 +19,7 @@ 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/default/files... +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 @@ -31,6 +31,6 @@ second-update-name 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/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-update-[UNIQUE_NAME]/default Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/bind-and-update/script b/acceptance/bundle/deploy/bind/bind-and-update/script index 1424e8b5b1c..60a64f29814 100644 --- a/acceptance/bundle/deploy/bind/bind-and-update/script +++ b/acceptance/bundle/deploy/bind/bind-and-update/script @@ -1,3 +1,5 @@ +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 diff --git a/acceptance/bundle/deploy/bind/block-removed/databricks.yml b/acceptance/bundle/deploy/bind/block-removed/databricks.yml index 10172bca10b..cb7b2234af8 100644 --- a/acceptance/bundle/deploy/bind/block-removed/databricks.yml +++ b/acceptance/bundle/deploy/bind/block-removed/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-block-removed + name: test-bind-block-removed-UNIQUE_NAME resources: pipelines: diff --git a/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml b/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml index 5f2d3861c56..1632bfce959 100644 --- a/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml +++ b/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-block-removed + name: test-bind-block-removed-UNIQUE_NAME resources: pipelines: diff --git a/acceptance/bundle/deploy/bind/block-removed/output.txt b/acceptance/bundle/deploy/bind/block-removed/output.txt index 2cee5430072..33cba18f69d 100644 --- a/acceptance/bundle/deploy/bind/block-removed/output.txt +++ b/acceptance/bundle/deploy/bind/block-removed/output.txt @@ -1,6 +1,6 @@ >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-block-removed/default/files... +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 @@ -19,6 +19,6 @@ This action will result in the deletion of the following Lakeflow Spark Declarat 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/default +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 diff --git a/acceptance/bundle/deploy/bind/block-removed/script b/acceptance/bundle/deploy/bind/block-removed/script index 41072fbc79c..4b75a1748be 100644 --- a/acceptance/bundle/deploy/bind/block-removed/script +++ b/acceptance/bundle/deploy/bind/block-removed/script @@ -1,3 +1,6 @@ +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 diff --git a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks.yml b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks.yml index cbb7da54afe..ae172299eed 100644 --- a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks.yml +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-delete-conflict + name: test-bind-delete-conflict-UNIQUE_NAME resources: jobs: @@ -14,6 +14,3 @@ resources: environment_key: default spark_python_task: python_file: ./hello.py - -targets: - default: diff --git a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks_conflict.yml b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks_conflict.yml index 0f6b6845c76..0232d1f092b 100644 --- a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks_conflict.yml +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/databricks_conflict.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-delete-conflict + name: test-bind-delete-conflict-UNIQUE_NAME resources: jobs: diff --git a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/output.txt b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/output.txt index 8e51004004d..065a7f6cbc7 100644 --- a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/output.txt +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/output.txt @@ -1,6 +1,6 @@ >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-delete-conflict/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-delete-conflict-[UNIQUE_NAME]/default/files... Created jobs.foo Files: 3 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged @@ -17,6 +17,6 @@ Error: bind validation failed 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-delete-conflict/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-delete-conflict-[UNIQUE_NAME]/default Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/script b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/script index 6547472f40f..df869e60a36 100644 --- a/acceptance/bundle/deploy/bind/delete-and-bind-conflict/script +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/script @@ -1,3 +1,6 @@ +update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME" +update_file.py databricks_conflict.yml 'UNIQUE_NAME' "$UNIQUE_NAME" + # Deploy foo to create it in state trace $CLI bundle deploy --auto-approve diff --git a/acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml b/acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml index 8abdbe2c8ca..59f39207f6a 100644 --- a/acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml +++ b/acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-destroy-allowed + name: test-bind-destroy-allowed-UNIQUE_NAME resources: jobs: diff --git a/acceptance/bundle/deploy/bind/destroy-allowed/output.txt b/acceptance/bundle/deploy/bind/destroy-allowed/output.txt index 476823d966c..88283729e0a 100644 --- a/acceptance/bundle/deploy/bind/destroy-allowed/output.txt +++ b/acceptance/bundle/deploy/bind/destroy-allowed/output.txt @@ -1,6 +1,6 @@ >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-destroy-allowed/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-destroy-allowed-[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 @@ -9,6 +9,6 @@ Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound 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-destroy-allowed/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-destroy-allowed-[UNIQUE_NAME]/default Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/destroy-allowed/script b/acceptance/bundle/deploy/bind/destroy-allowed/script index 75d366070f9..2ca53089a89 100644 --- a/acceptance/bundle/deploy/bind/destroy-allowed/script +++ b/acceptance/bundle/deploy/bind/destroy-allowed/script @@ -1,3 +1,5 @@ +update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME" + # Create a job in the workspace NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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 diff --git a/acceptance/bundle/deploy/bind/dev-mode/output.txt b/acceptance/bundle/deploy/bind/dev-mode/output.txt index 36af0f1d8ce..0cd894c31d8 100644 --- a/acceptance/bundle/deploy/bind/dev-mode/output.txt +++ b/acceptance/bundle/deploy/bind/dev-mode/output.txt @@ -5,10 +5,6 @@ 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 plan -o json -json.plan.resources.jobs.foo.changes.name.action = "update"; json.plan.resources.jobs.foo.changes.name.new = "[dev [USERNAME]] test-bind-job"; -json.plan.resources.jobs.foo.changes.name.remote = "test-bind-job"; -json.plan.resources.jobs.foo.changes.schedule.pause_status.action = "update"; json.plan.resources.jobs.foo.changes.schedule.pause_status.new = "PAUSED"; -json.plan.resources.jobs.foo.changes.tags.action = "update"; json.plan.resources.jobs.foo.changes.tags.new.dev = "[USERNAME]"; diff --git a/acceptance/bundle/deploy/bind/dev-mode/script b/acceptance/bundle/deploy/bind/dev-mode/script index 3bdacd14b1a..63f6344a41c 100644 --- a/acceptance/bundle/deploy/bind/dev-mode/script +++ b/acceptance/bundle/deploy/bind/dev-mode/script @@ -1,9 +1,11 @@ # Binding in development mode applies the mode's transformations to the adopted # resource: it is renamed with the [dev ] prefix, its schedule is paused and # it is tagged. The plan is the only place that shows this before it happens. +# Only the new values are listed: the remote side differs between the test server +# and a real workspace (which reports pause_status: UNPAUSED). NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-job", "schedule": {"quartz_cron_expression": "0 0 12 * * ?", "timezone_id": "UTC"}, "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_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" trace $CLI bundle plan -trace $CLI bundle plan -o json | gron.py | grep -E 'changes\.(name|schedule|tags)' +trace $CLI bundle plan -o json | gron.py | grep -E 'changes\.(name|schedule|tags)\S*\.new' diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks.yml b/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks.yml index 9ae092f1907..6db6bc7d2ed 100644 --- a/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks.yml +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-duplicate-id + name: test-bind-duplicate-id-UNIQUE_NAME resources: jobs: @@ -14,6 +14,3 @@ resources: environment_key: default spark_python_task: python_file: ./hello.py - -targets: - default: diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks_with_bind.yml b/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks_with_bind.yml index f852ac6c9a0..234b22fd66b 100644 --- a/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks_with_bind.yml +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/databricks_with_bind.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-duplicate-id + name: test-bind-duplicate-id-UNIQUE_NAME resources: jobs: diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id/output.txt b/acceptance/bundle/deploy/bind/duplicate-bind-id/output.txt index b24d715a924..744bb060309 100644 --- a/acceptance/bundle/deploy/bind/duplicate-bind-id/output.txt +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/output.txt @@ -1,6 +1,6 @@ >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-duplicate-id/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-duplicate-id-[UNIQUE_NAME]/default/files... Created jobs.foo Files: 3 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged @@ -17,6 +17,6 @@ Error: bind validation failed 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-duplicate-id/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-duplicate-id-[UNIQUE_NAME]/default Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/duplicate-bind-id/script b/acceptance/bundle/deploy/bind/duplicate-bind-id/script index 57ab78fcf0f..4ca237bffb0 100644 --- a/acceptance/bundle/deploy/bind/duplicate-bind-id/script +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/script @@ -1,3 +1,6 @@ +update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME" +update_file.py databricks_with_bind.yml 'UNIQUE_NAME' "$UNIQUE_NAME" + # Deploy foo to create it in state trace $CLI bundle deploy --auto-approve diff --git a/acceptance/bundle/deploy/bind/gone-in-state/databricks.yml b/acceptance/bundle/deploy/bind/gone-in-state/databricks.yml index f5df89d5945..297233c545e 100644 --- a/acceptance/bundle/deploy/bind/gone-in-state/databricks.yml +++ b/acceptance/bundle/deploy/bind/gone-in-state/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-gone-in-state + name: test-bind-gone-in-state-UNIQUE_NAME resources: jobs: diff --git a/acceptance/bundle/deploy/bind/gone-in-state/output.txt b/acceptance/bundle/deploy/bind/gone-in-state/output.txt index 6747e7d05a1..bfebd09896d 100644 --- a/acceptance/bundle/deploy/bind/gone-in-state/output.txt +++ b/acceptance/bundle/deploy/bind/gone-in-state/output.txt @@ -1,6 +1,6 @@ >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-gone-in-state/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-gone-in-state-[UNIQUE_NAME]/default/files... Bound and updated jobs.foo Files: 1 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound @@ -15,7 +15,7 @@ Error: planning failed >>> musterr [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-gone-in-state/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-gone-in-state-[UNIQUE_NAME]/default/files... Error: cannot plan resources.jobs.foo: bound resource with ID "[NEW_JOB_ID]" no longer exists in the workspace; remove lifecycle.bind to create a new resource Error: planning failed @@ -23,6 +23,6 @@ Error: planning failed Files: 0 uploaded, 0 deleted >>> [CLI] bundle destroy --auto-approve -All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-gone-in-state/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-gone-in-state-[UNIQUE_NAME]/default Destroy: 0 deleted diff --git a/acceptance/bundle/deploy/bind/gone-in-state/script b/acceptance/bundle/deploy/bind/gone-in-state/script index db0c9fdcfc9..049e1cce043 100644 --- a/acceptance/bundle/deploy/bind/gone-in-state/script +++ b/acceptance/bundle/deploy/bind/gone-in-state/script @@ -1,3 +1,5 @@ +update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME" + # Create a job in the workspace and bind it NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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 diff --git a/acceptance/bundle/deploy/bind/id-collision-different-type/databricks.yml b/acceptance/bundle/deploy/bind/id-collision-different-type/databricks.yml index d0c26c5697b..8e5f17f4c89 100644 --- a/acceptance/bundle/deploy/bind/id-collision-different-type/databricks.yml +++ b/acceptance/bundle/deploy/bind/id-collision-different-type/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-id-collision-type + name: test-bind-id-collision-type-UNIQUE_NAME resources: jobs: @@ -14,6 +14,3 @@ resources: environment_key: default spark_python_task: python_file: /Workspace/test.py - -targets: - default: diff --git a/acceptance/bundle/deploy/bind/id-collision-different-type/databricks_with_bind.yml b/acceptance/bundle/deploy/bind/id-collision-different-type/databricks_with_bind.yml index 6e5af0ee0db..06987427d19 100644 --- a/acceptance/bundle/deploy/bind/id-collision-different-type/databricks_with_bind.yml +++ b/acceptance/bundle/deploy/bind/id-collision-different-type/databricks_with_bind.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-id-collision-type + name: test-bind-id-collision-type-UNIQUE_NAME resources: jobs: diff --git a/acceptance/bundle/deploy/bind/id-collision-different-type/output.txt b/acceptance/bundle/deploy/bind/id-collision-different-type/output.txt index f425e23564d..4ef65c527d3 100644 --- a/acceptance/bundle/deploy/bind/id-collision-different-type/output.txt +++ b/acceptance/bundle/deploy/bind/id-collision-different-type/output.txt @@ -1,6 +1,6 @@ >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-id-collision-type/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-id-collision-type-[UNIQUE_NAME]/default/files... Created jobs.foo Files: 2 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged @@ -15,6 +15,6 @@ Error: planning failed 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-id-collision-type/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-id-collision-type-[UNIQUE_NAME]/default Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/id-collision-different-type/script b/acceptance/bundle/deploy/bind/id-collision-different-type/script index 6c4bb5411fe..ca4191ec385 100644 --- a/acceptance/bundle/deploy/bind/id-collision-different-type/script +++ b/acceptance/bundle/deploy/bind/id-collision-different-type/script @@ -1,3 +1,6 @@ +update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME" +update_file.py databricks_with_bind.yml 'UNIQUE_NAME' "$UNIQUE_NAME" + # Deploy foo to create it in state trace $CLI bundle deploy --auto-approve diff --git a/acceptance/bundle/deploy/bind/pure-bind/databricks.yml b/acceptance/bundle/deploy/bind/pure-bind/databricks.yml index 2d23ceb407c..1dc0f3557c2 100644 --- a/acceptance/bundle/deploy/bind/pure-bind/databricks.yml +++ b/acceptance/bundle/deploy/bind/pure-bind/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-pure + name: test-bind-pure-UNIQUE_NAME resources: jobs: diff --git a/acceptance/bundle/deploy/bind/pure-bind/output.txt b/acceptance/bundle/deploy/bind/pure-bind/output.txt index 32e0f9dd517..6ad11bb2f42 100644 --- a/acceptance/bundle/deploy/bind/pure-bind/output.txt +++ b/acceptance/bundle/deploy/bind/pure-bind/output.txt @@ -1,6 +1,6 @@ >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-pure/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-pure-[UNIQUE_NAME]/default/files... Bound and updated jobs.foo Created jobs.foo.permissions Files: 1 uploaded, 0 deleted @@ -31,7 +31,7 @@ json.plan.resources.jobs.foo.action = "bind"; {"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-pure/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-pure-[UNIQUE_NAME]/default/files... Bound jobs.foo Created jobs.foo.permissions Files: 0 uploaded, 0 deleted @@ -49,6 +49,6 @@ json.state.resources.jobs.foo.permissions.__id__ = "/jobs/[NEW_JOB_ID]"; 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-pure/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-pure-[UNIQUE_NAME]/default Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/pure-bind/script b/acceptance/bundle/deploy/bind/pure-bind/script index 7389ece5538..45a1f4a056b 100644 --- a/acceptance/bundle/deploy/bind/pure-bind/script +++ b/acceptance/bundle/deploy/bind/pure-bind/script @@ -1,3 +1,5 @@ +update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME" + # Create a job in the workspace and bind it; the first deploy is a bind_and_update # because the bundle sets fields such as deployment and edit_mode on every job. NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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) diff --git a/acceptance/bundle/deploy/bind/rebind-different-id/databricks.yml b/acceptance/bundle/deploy/bind/rebind-different-id/databricks.yml index 23de6561b8a..34c1ecf3a82 100644 --- a/acceptance/bundle/deploy/bind/rebind-different-id/databricks.yml +++ b/acceptance/bundle/deploy/bind/rebind-different-id/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-rebind-different-id + name: test-bind-rebind-different-id-UNIQUE_NAME resources: jobs: @@ -14,6 +14,3 @@ resources: environment_key: default spark_python_task: python_file: /Workspace/test.py - -targets: - default: diff --git a/acceptance/bundle/deploy/bind/rebind-different-id/databricks_with_bind.yml b/acceptance/bundle/deploy/bind/rebind-different-id/databricks_with_bind.yml index 459b71e1445..40e85d37a0e 100644 --- a/acceptance/bundle/deploy/bind/rebind-different-id/databricks_with_bind.yml +++ b/acceptance/bundle/deploy/bind/rebind-different-id/databricks_with_bind.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-rebind-different-id + name: test-bind-rebind-different-id-UNIQUE_NAME resources: jobs: diff --git a/acceptance/bundle/deploy/bind/rebind-different-id/output.txt b/acceptance/bundle/deploy/bind/rebind-different-id/output.txt index a21c3ab9b89..1bbd0c4cc7b 100644 --- a/acceptance/bundle/deploy/bind/rebind-different-id/output.txt +++ b/acceptance/bundle/deploy/bind/rebind-different-id/output.txt @@ -1,6 +1,6 @@ >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-rebind-different-id/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-rebind-different-id-[UNIQUE_NAME]/default/files... Created jobs.foo Files: 2 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged @@ -18,6 +18,6 @@ Error: bind validation failed 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-rebind-different-id/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-rebind-different-id-[UNIQUE_NAME]/default Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/rebind-different-id/script b/acceptance/bundle/deploy/bind/rebind-different-id/script index 6653c8a6e2a..c5d08c6a07c 100644 --- a/acceptance/bundle/deploy/bind/rebind-different-id/script +++ b/acceptance/bundle/deploy/bind/rebind-different-id/script @@ -1,3 +1,6 @@ +update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME" +update_file.py databricks_with_bind.yml 'UNIQUE_NAME' "$UNIQUE_NAME" + # Deploy foo to create it in state trace $CLI bundle deploy --auto-approve JOB_ID=$(read_id.py foo) diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/databricks.yml b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/databricks.yml index c8385967447..9bec38b6a6b 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/databricks.yml +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-recreate-in-state + name: test-bind-recreate-in-state-UNIQUE_NAME resources: pipelines: diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt index d9b479d7054..69a32ffc21c 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt @@ -1,6 +1,6 @@ >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-recreate-in-state/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-recreate-in-state-[UNIQUE_NAME]/default/files... Bound and updated pipelines.foo Files: 2 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound @@ -35,3 +35,15 @@ To resolve this issue, you have two options: Error: planning failed + +>>> [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-recreate-in-state-[UNIQUE_NAME]/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/script b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/script index a0909528b1c..517c9ed3949 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/script +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/script @@ -1,3 +1,5 @@ +update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME" + # Create a pipeline whose storage matches the config, so the first deploy binds # it cleanly (no recreate). 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) @@ -13,3 +15,7 @@ trace $CLI bundle deploy --auto-approve # block must keep guarding against recreate even after the resource is in state. update_file.py databricks.yml '/old/storage/path' '/new/storage/path' trace musterr $CLI bundle plan + +# Cleanup +update_file.py databricks.yml '/new/storage/path' '/old/storage/path' +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deploy/bind/ref-to-bound/databricks.yml b/acceptance/bundle/deploy/bind/ref-to-bound/databricks.yml index f922fdc4e9b..5218a0c2a62 100644 --- a/acceptance/bundle/deploy/bind/ref-to-bound/databricks.yml +++ b/acceptance/bundle/deploy/bind/ref-to-bound/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-ref-to-bound + name: test-bind-ref-to-bound-UNIQUE_NAME resources: jobs: diff --git a/acceptance/bundle/deploy/bind/ref-to-bound/output.txt b/acceptance/bundle/deploy/bind/ref-to-bound/output.txt index ed7be1ceb39..10dcb40bbcb 100644 --- a/acceptance/bundle/deploy/bind/ref-to-bound/output.txt +++ b/acceptance/bundle/deploy/bind/ref-to-bound/output.txt @@ -10,7 +10,7 @@ Plan: 1 to add, 1 to change, 0 to delete, 0 unchanged, 1 to bind json.plan.resources.jobs.bar.new_state.value.tasks[0].run_job_task.job_id = [NEW_JOB_ID]; >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-ref-to-bound/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-ref-to-bound-[UNIQUE_NAME]/default/files... Bound and updated jobs.foo Created jobs.bar Files: 1 uploaded, 0 deleted @@ -24,6 +24,6 @@ The following resources will be deleted: delete resources.jobs.bar delete resources.jobs.foo -All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-ref-to-bound/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-ref-to-bound-[UNIQUE_NAME]/default Destroy: 2 deleted diff --git a/acceptance/bundle/deploy/bind/ref-to-bound/script b/acceptance/bundle/deploy/bind/ref-to-bound/script index 56a4d70d45e..e033312fe5e 100644 --- a/acceptance/bundle/deploy/bind/ref-to-bound/script +++ b/acceptance/bundle/deploy/bind/ref-to-bound/script @@ -1,3 +1,5 @@ +update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME" + # Create a job in the workspace; bar refers to it through ${resources.jobs.foo.id} NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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 diff --git a/acceptance/bundle/deploy/bind/removed-from-config/databricks.yml b/acceptance/bundle/deploy/bind/removed-from-config/databricks.yml index ff2cbca35cb..b9058bbe0c6 100644 --- a/acceptance/bundle/deploy/bind/removed-from-config/databricks.yml +++ b/acceptance/bundle/deploy/bind/removed-from-config/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-removed-from-config + name: test-bind-removed-from-config-UNIQUE_NAME resources: jobs: diff --git a/acceptance/bundle/deploy/bind/removed-from-config/databricks_nores.yml b/acceptance/bundle/deploy/bind/removed-from-config/databricks_nores.yml index 6e007493827..9b759d9f81f 100644 --- a/acceptance/bundle/deploy/bind/removed-from-config/databricks_nores.yml +++ b/acceptance/bundle/deploy/bind/removed-from-config/databricks_nores.yml @@ -1,2 +1,2 @@ bundle: - name: test-bind-removed-from-config + name: test-bind-removed-from-config-UNIQUE_NAME diff --git a/acceptance/bundle/deploy/bind/removed-from-config/output.txt b/acceptance/bundle/deploy/bind/removed-from-config/output.txt index 7562bbb76d7..c7e97a1b674 100644 --- a/acceptance/bundle/deploy/bind/removed-from-config/output.txt +++ b/acceptance/bundle/deploy/bind/removed-from-config/output.txt @@ -1,6 +1,6 @@ >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-removed-from-config/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-removed-from-config-[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 @@ -12,7 +12,7 @@ delete jobs.foo Plan: 0 to add, 0 to change, 1 to delete, 0 unchanged >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-removed-from-config/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-removed-from-config-[UNIQUE_NAME]/default/files... Deleted jobs.foo Files: 1 uploaded, 0 deleted Resources: 0 created, 0 changed, 1 deleted, 0 unchanged @@ -21,6 +21,6 @@ Resources: 0 created, 0 changed, 1 deleted, 0 unchanged Error: Job [NEW_JOB_ID] does not exist. >>> [CLI] bundle destroy --auto-approve -All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-removed-from-config/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-removed-from-config-[UNIQUE_NAME]/default Destroy: 0 deleted diff --git a/acceptance/bundle/deploy/bind/removed-from-config/script b/acceptance/bundle/deploy/bind/removed-from-config/script index 1eddff9c0b7..082c5fa089f 100644 --- a/acceptance/bundle/deploy/bind/removed-from-config/script +++ b/acceptance/bundle/deploy/bind/removed-from-config/script @@ -1,3 +1,6 @@ +update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME" +update_file.py databricks_nores.yml 'UNIQUE_NAME' "$UNIQUE_NAME" + # Create a job in the workspace and bind it NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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 diff --git a/acceptance/bundle/deploy/bind/variable-id/databricks.yml b/acceptance/bundle/deploy/bind/variable-id/databricks.yml index cbc7dd4413e..f266dec8262 100644 --- a/acceptance/bundle/deploy/bind/variable-id/databricks.yml +++ b/acceptance/bundle/deploy/bind/variable-id/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-variable-id + name: test-bind-variable-id-UNIQUE_NAME variables: job_id: diff --git a/acceptance/bundle/deploy/bind/variable-id/output.txt b/acceptance/bundle/deploy/bind/variable-id/output.txt index 89f80ee5f95..99a13d92977 100644 --- a/acceptance/bundle/deploy/bind/variable-id/output.txt +++ b/acceptance/bundle/deploy/bind/variable-id/output.txt @@ -5,7 +5,7 @@ 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-variable-id/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-variable-id-[UNIQUE_NAME]/default/files... Bound and updated jobs.foo Files: 1 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound @@ -17,6 +17,6 @@ json.state.resources.jobs.foo.__id__ = "[NEW_JOB_ID]"; 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-variable-id/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-variable-id-[UNIQUE_NAME]/default Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/variable-id/script b/acceptance/bundle/deploy/bind/variable-id/script index 2a1f388bd63..0e9a511a8e2 100644 --- a/acceptance/bundle/deploy/bind/variable-id/script +++ b/acceptance/bundle/deploy/bind/variable-id/script @@ -1,3 +1,5 @@ +update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME" + # Create a job in the workspace; its ID reaches the bind block through a variable NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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 diff --git a/bundle/direct/dresources/type_test.go b/bundle/direct/dresources/type_test.go index d7515ae55a8..38b758b9625 100644 --- a/bundle/direct/dresources/type_test.go +++ b/bundle/direct/dresources/type_test.go @@ -83,19 +83,23 @@ var knownMissingInStateType = map[string][]string{ "apps": { "lifecycle.prevent_destroy", "lifecycle.depends_on", + "lifecycle.bind", }, "clusters": { "lifecycle.prevent_destroy", "lifecycle.depends_on", + "lifecycle.bind", }, "sql_warehouses": { "lifecycle.prevent_destroy", "lifecycle.depends_on", + "lifecycle.bind", }, "job_runs": { // State stores trigger fingerprints, not the config trigger list / prevent_destroy. "lifecycle.prevent_destroy", "lifecycle.depends_on", + "lifecycle.bind", "lifecycle.triggers", }, "dashboards": { diff --git a/python/databricks/bundles/alerts/__init__.py b/python/databricks/bundles/alerts/__init__.py index edc2e7294d9..4785eeb3caa 100644 --- a/python/databricks/bundles/alerts/__init__.py +++ b/python/databricks/bundles/alerts/__init__.py @@ -32,6 +32,9 @@ "AlertV2Subscription", "AlertV2SubscriptionDict", "AlertV2SubscriptionParam", + "Bind", + "BindDict", + "BindParam", "ComparisonOperator", "ComparisonOperatorParam", "CronSchedule", @@ -101,6 +104,7 @@ AlertV2SubscriptionDict, AlertV2SubscriptionParam, ) +from databricks.bundles.alerts._models.bind import Bind, BindDict, BindParam from databricks.bundles.alerts._models.comparison_operator import ( ComparisonOperator, ComparisonOperatorParam, diff --git a/python/databricks/bundles/alerts/_models/alert.py b/python/databricks/bundles/alerts/_models/alert.py index e6dbf9d8448..1924b0f035a 100644 --- a/python/databricks/bundles/alerts/_models/alert.py +++ b/python/databricks/bundles/alerts/_models/alert.py @@ -23,10 +23,7 @@ CustomTemplateFormat, CustomTemplateFormatParam, ) -from databricks.bundles.alerts._models.lifecycle import ( - Lifecycle, - LifecycleParam, -) +from databricks.bundles.alerts._models.lifecycle import Lifecycle, LifecycleParam from databricks.bundles.alerts._models.permission import Permission, PermissionParam from databricks.bundles.core._resource import Resource from databricks.bundles.core._transform import _transform diff --git a/python/databricks/bundles/alerts/_models/bind.py b/python/databricks/bundles/alerts/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/alerts/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/alerts/_models/lifecycle.py b/python/databricks/bundles/alerts/_models/lifecycle.py index 3f8bb9dec50..3e0828f5e69 100644 --- a/python/databricks/bundles/alerts/_models/lifecycle.py +++ b/python/databricks/bundles/alerts/_models/lifecycle.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, TypedDict +from databricks.bundles.alerts._models.bind import Bind, BindParam from databricks.bundles.alerts._models.depends_on import DependsOn, DependsOnParam from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value @@ -16,6 +17,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -37,6 +43,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/apps/__init__.py b/python/databricks/bundles/apps/__init__.py index 944c7424fd5..4d1c18730cb 100644 --- a/python/databricks/bundles/apps/__init__.py +++ b/python/databricks/bundles/apps/__init__.py @@ -70,6 +70,9 @@ "AppResourceUcSecurableUcSecurablePermissionParam", "AppResourceUcSecurableUcSecurableType", "AppResourceUcSecurableUcSecurableTypeParam", + "Bind", + "BindDict", + "BindParam", "ComputeSize", "ComputeSizeParam", "DependsOn", @@ -212,6 +215,7 @@ AppResourceUcSecurableUcSecurableType, AppResourceUcSecurableUcSecurableTypeParam, ) +from databricks.bundles.apps._models.bind import Bind, BindDict, BindParam from databricks.bundles.apps._models.compute_size import ComputeSize, ComputeSizeParam from databricks.bundles.apps._models.depends_on import ( DependsOn, diff --git a/python/databricks/bundles/apps/_models/bind.py b/python/databricks/bundles/apps/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/apps/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/apps/_models/lifecycle_with_started.py b/python/databricks/bundles/apps/_models/lifecycle_with_started.py index 0784bf0e2e7..cefac435c97 100644 --- a/python/databricks/bundles/apps/_models/lifecycle_with_started.py +++ b/python/databricks/bundles/apps/_models/lifecycle_with_started.py @@ -3,10 +3,8 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, TypedDict -from databricks.bundles.apps._models.depends_on import ( - DependsOn, - DependsOnParam, -) +from databricks.bundles.apps._models.bind import Bind, BindParam +from databricks.bundles.apps._models.depends_on import DependsOn, DependsOnParam from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional @@ -19,6 +17,11 @@ class LifecycleWithStarted: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -45,6 +48,11 @@ def as_dict(self) -> "LifecycleWithStartedDict": class LifecycleWithStartedDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/catalogs/__init__.py b/python/databricks/bundles/catalogs/__init__.py index fced08f1bb2..da17929ec74 100644 --- a/python/databricks/bundles/catalogs/__init__.py +++ b/python/databricks/bundles/catalogs/__init__.py @@ -4,6 +4,9 @@ "AzureEncryptionSettings", "AzureEncryptionSettingsDict", "AzureEncryptionSettingsParam", + "Bind", + "BindDict", + "BindParam", "Catalog", "CatalogDict", "CatalogGrant", @@ -34,6 +37,7 @@ AzureEncryptionSettingsDict, AzureEncryptionSettingsParam, ) +from databricks.bundles.catalogs._models.bind import Bind, BindDict, BindParam from databricks.bundles.catalogs._models.catalog import ( Catalog, CatalogDict, diff --git a/python/databricks/bundles/catalogs/_models/bind.py b/python/databricks/bundles/catalogs/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/catalogs/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/catalogs/_models/catalog.py b/python/databricks/bundles/catalogs/_models/catalog.py index 1f8f3e95b7a..bdb397b0cb0 100644 --- a/python/databricks/bundles/catalogs/_models/catalog.py +++ b/python/databricks/bundles/catalogs/_models/catalog.py @@ -7,10 +7,7 @@ EncryptionSettings, EncryptionSettingsParam, ) -from databricks.bundles.catalogs._models.lifecycle import ( - Lifecycle, - LifecycleParam, -) +from databricks.bundles.catalogs._models.lifecycle import Lifecycle, LifecycleParam from databricks.bundles.catalogs._models.privilege_assignment import ( PrivilegeAssignment, PrivilegeAssignmentParam, diff --git a/python/databricks/bundles/catalogs/_models/lifecycle.py b/python/databricks/bundles/catalogs/_models/lifecycle.py index 0a9985c5b30..067d2787d96 100644 --- a/python/databricks/bundles/catalogs/_models/lifecycle.py +++ b/python/databricks/bundles/catalogs/_models/lifecycle.py @@ -3,7 +3,11 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, TypedDict -from databricks.bundles.catalogs._models.depends_on import DependsOn, DependsOnParam +from databricks.bundles.catalogs._models.bind import Bind, BindParam +from databricks.bundles.catalogs._models.depends_on import ( + DependsOn, + DependsOnParam, +) from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional @@ -16,6 +20,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -37,6 +46,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/cluster_policies/__init__.py b/python/databricks/bundles/cluster_policies/__init__.py index 5f5790166f7..92e19472d4d 100644 --- a/python/databricks/bundles/cluster_policies/__init__.py +++ b/python/databricks/bundles/cluster_policies/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "ClusterPolicy", "ClusterPolicyDict", "ClusterPolicyParam", @@ -30,6 +33,7 @@ ] +from databricks.bundles.cluster_policies._models.bind import Bind, BindDict, BindParam from databricks.bundles.cluster_policies._models.cluster_policy import ( ClusterPolicy, ClusterPolicyDict, diff --git a/python/databricks/bundles/cluster_policies/_models/bind.py b/python/databricks/bundles/cluster_policies/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/cluster_policies/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/cluster_policies/_models/lifecycle.py b/python/databricks/bundles/cluster_policies/_models/lifecycle.py index ccec29db600..e38185faedf 100644 --- a/python/databricks/bundles/cluster_policies/_models/lifecycle.py +++ b/python/databricks/bundles/cluster_policies/_models/lifecycle.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, TypedDict +from databricks.bundles.cluster_policies._models.bind import Bind, BindParam from databricks.bundles.cluster_policies._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +20,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +46,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/clusters/__init__.py b/python/databricks/bundles/clusters/__init__.py index feaa262198f..3796dee5368 100644 --- a/python/databricks/bundles/clusters/__init__.py +++ b/python/databricks/bundles/clusters/__init__.py @@ -17,6 +17,9 @@ "AzureAttributesParam", "AzureAvailability", "AzureAvailabilityParam", + "Bind", + "BindDict", + "BindParam", "ClientsTypes", "ClientsTypesDict", "ClientsTypesParam", @@ -133,6 +136,7 @@ AzureAvailability, AzureAvailabilityParam, ) +from databricks.bundles.clusters._models.bind import Bind, BindDict, BindParam from databricks.bundles.clusters._models.clients_types import ( ClientsTypes, ClientsTypesDict, diff --git a/python/databricks/bundles/clusters/_models/bind.py b/python/databricks/bundles/clusters/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/clusters/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/clusters/_models/lifecycle_with_started.py b/python/databricks/bundles/clusters/_models/lifecycle_with_started.py index 68aa5088083..7a932c06d88 100644 --- a/python/databricks/bundles/clusters/_models/lifecycle_with_started.py +++ b/python/databricks/bundles/clusters/_models/lifecycle_with_started.py @@ -3,10 +3,8 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, TypedDict -from databricks.bundles.clusters._models.depends_on import ( - DependsOn, - DependsOnParam, -) +from databricks.bundles.clusters._models.bind import Bind, BindParam +from databricks.bundles.clusters._models.depends_on import DependsOn, DependsOnParam from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional @@ -19,6 +17,11 @@ class LifecycleWithStarted: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -45,6 +48,11 @@ def as_dict(self) -> "LifecycleWithStartedDict": class LifecycleWithStartedDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/dashboards/__init__.py b/python/databricks/bundles/dashboards/__init__.py index 7e383631fb2..233b0015ffc 100644 --- a/python/databricks/bundles/dashboards/__init__.py +++ b/python/databricks/bundles/dashboards/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "Dashboard", "DashboardDict", "DashboardParam", @@ -20,6 +23,7 @@ ] +from databricks.bundles.dashboards._models.bind import Bind, BindDict, BindParam from databricks.bundles.dashboards._models.dashboard import ( Dashboard, DashboardDict, diff --git a/python/databricks/bundles/dashboards/_models/bind.py b/python/databricks/bundles/dashboards/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/dashboards/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/dashboards/_models/dashboard.py b/python/databricks/bundles/dashboards/_models/dashboard.py index d7efbdf814f..2a3e5f03364 100644 --- a/python/databricks/bundles/dashboards/_models/dashboard.py +++ b/python/databricks/bundles/dashboards/_models/dashboard.py @@ -7,10 +7,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional -from databricks.bundles.dashboards._models.lifecycle import ( - Lifecycle, - LifecycleParam, -) +from databricks.bundles.dashboards._models.lifecycle import Lifecycle, LifecycleParam from databricks.bundles.dashboards._models.lifecycle_state import ( LifecycleState, LifecycleStateParam, diff --git a/python/databricks/bundles/dashboards/_models/lifecycle.py b/python/databricks/bundles/dashboards/_models/lifecycle.py index fa61e0279a4..39198b5521e 100644 --- a/python/databricks/bundles/dashboards/_models/lifecycle.py +++ b/python/databricks/bundles/dashboards/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.dashboards._models.bind import Bind, BindParam from databricks.bundles.dashboards._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +20,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +46,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/database_catalogs/__init__.py b/python/databricks/bundles/database_catalogs/__init__.py index a02e6710b39..633b373d88d 100644 --- a/python/databricks/bundles/database_catalogs/__init__.py +++ b/python/databricks/bundles/database_catalogs/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DatabaseCatalog", "DatabaseCatalogDict", "DatabaseCatalogParam", @@ -13,6 +16,7 @@ ] +from databricks.bundles.database_catalogs._models.bind import Bind, BindDict, BindParam from databricks.bundles.database_catalogs._models.database_catalog import ( DatabaseCatalog, DatabaseCatalogDict, diff --git a/python/databricks/bundles/database_catalogs/_models/bind.py b/python/databricks/bundles/database_catalogs/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/database_catalogs/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/database_catalogs/_models/lifecycle.py b/python/databricks/bundles/database_catalogs/_models/lifecycle.py index 098d125a985..021237f4da7 100644 --- a/python/databricks/bundles/database_catalogs/_models/lifecycle.py +++ b/python/databricks/bundles/database_catalogs/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.database_catalogs._models.bind import Bind, BindParam from databricks.bundles.database_catalogs._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +20,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +46,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/database_instances/__init__.py b/python/databricks/bundles/database_instances/__init__.py index f6b05ef2765..f92fd55c82c 100644 --- a/python/databricks/bundles/database_instances/__init__.py +++ b/python/databricks/bundles/database_instances/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "CustomTag", "CustomTagDict", "CustomTagParam", @@ -24,6 +27,7 @@ ] +from databricks.bundles.database_instances._models.bind import Bind, BindDict, BindParam from databricks.bundles.database_instances._models.custom_tag import ( CustomTag, CustomTagDict, diff --git a/python/databricks/bundles/database_instances/_models/bind.py b/python/databricks/bundles/database_instances/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/database_instances/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/database_instances/_models/lifecycle.py b/python/databricks/bundles/database_instances/_models/lifecycle.py index 2e81c847cc6..f74e28445d1 100644 --- a/python/databricks/bundles/database_instances/_models/lifecycle.py +++ b/python/databricks/bundles/database_instances/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.database_instances._models.bind import Bind, BindParam from databricks.bundles.database_instances._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +20,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +46,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/experiments/__init__.py b/python/databricks/bundles/experiments/__init__.py index 78726dd8002..e6b01f08927 100644 --- a/python/databricks/bundles/experiments/__init__.py +++ b/python/databricks/bundles/experiments/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -27,6 +30,7 @@ ] +from databricks.bundles.experiments._models.bind import Bind, BindDict, BindParam from databricks.bundles.experiments._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/experiments/_models/bind.py b/python/databricks/bundles/experiments/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/experiments/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/experiments/_models/lifecycle.py b/python/databricks/bundles/experiments/_models/lifecycle.py index f2b82bf9aa2..c70097e927b 100644 --- a/python/databricks/bundles/experiments/_models/lifecycle.py +++ b/python/databricks/bundles/experiments/_models/lifecycle.py @@ -6,10 +6,8 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional -from databricks.bundles.experiments._models.depends_on import ( - DependsOn, - DependsOnParam, -) +from databricks.bundles.experiments._models.bind import Bind, BindParam +from databricks.bundles.experiments._models.depends_on import DependsOn, DependsOnParam if TYPE_CHECKING: from typing_extensions import Self @@ -19,6 +17,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +43,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/experiments/_models/mlflow_experiment.py b/python/databricks/bundles/experiments/_models/mlflow_experiment.py index bafe167f2f8..8a9d5368e99 100644 --- a/python/databricks/bundles/experiments/_models/mlflow_experiment.py +++ b/python/databricks/bundles/experiments/_models/mlflow_experiment.py @@ -19,7 +19,10 @@ ExperimentTraceLocation, ExperimentTraceLocationParam, ) -from databricks.bundles.experiments._models.lifecycle import Lifecycle, LifecycleParam +from databricks.bundles.experiments._models.lifecycle import ( + Lifecycle, + LifecycleParam, +) from databricks.bundles.experiments._models.mlflow_experiment_permission import ( MlflowExperimentPermission, MlflowExperimentPermissionParam, diff --git a/python/databricks/bundles/external_locations/__init__.py b/python/databricks/bundles/external_locations/__init__.py index 18f867345b2..32ba39ab02f 100644 --- a/python/databricks/bundles/external_locations/__init__.py +++ b/python/databricks/bundles/external_locations/__init__.py @@ -7,6 +7,9 @@ "AzureQueueStorage", "AzureQueueStorageDict", "AzureQueueStorageParam", + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -48,6 +51,7 @@ AzureQueueStorageDict, AzureQueueStorageParam, ) +from databricks.bundles.external_locations._models.bind import Bind, BindDict, BindParam from databricks.bundles.external_locations._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/external_locations/_models/bind.py b/python/databricks/bundles/external_locations/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/external_locations/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/external_locations/_models/lifecycle.py b/python/databricks/bundles/external_locations/_models/lifecycle.py index ace07eb75a1..2b82a5ca4ac 100644 --- a/python/databricks/bundles/external_locations/_models/lifecycle.py +++ b/python/databricks/bundles/external_locations/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.external_locations._models.bind import Bind, BindParam from databricks.bundles.external_locations._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +20,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +46,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/features/__init__.py b/python/databricks/bundles/features/__init__.py index 28b95182245..fc9843349c8 100644 --- a/python/databricks/bundles/features/__init__.py +++ b/python/databricks/bundles/features/__init__.py @@ -13,6 +13,9 @@ "AvgFunction", "AvgFunctionDict", "AvgFunctionParam", + "Bind", + "BindDict", + "BindParam", "ColumnSelection", "ColumnSelectionDict", "ColumnSelectionParam", @@ -161,6 +164,7 @@ AvgFunctionDict, AvgFunctionParam, ) +from databricks.bundles.features._models.bind import Bind, BindDict, BindParam from databricks.bundles.features._models.column_selection import ( ColumnSelection, ColumnSelectionDict, diff --git a/python/databricks/bundles/features/_models/bind.py b/python/databricks/bundles/features/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/features/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/features/_models/feature.py b/python/databricks/bundles/features/_models/feature.py index b489a756bcc..d00ed799f92 100644 --- a/python/databricks/bundles/features/_models/feature.py +++ b/python/databricks/bundles/features/_models/feature.py @@ -23,7 +23,10 @@ Function, FunctionParam, ) -from databricks.bundles.features._models.lifecycle import Lifecycle, LifecycleParam +from databricks.bundles.features._models.lifecycle import ( + Lifecycle, + LifecycleParam, +) from databricks.bundles.features._models.lineage_context import ( LineageContext, LineageContextParam, diff --git a/python/databricks/bundles/features/_models/lifecycle.py b/python/databricks/bundles/features/_models/lifecycle.py index 53c3a4b38b9..088937d751a 100644 --- a/python/databricks/bundles/features/_models/lifecycle.py +++ b/python/databricks/bundles/features/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.features._models.bind import Bind, BindParam from databricks.bundles.features._models.depends_on import DependsOn, DependsOnParam if TYPE_CHECKING: @@ -16,6 +17,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -37,6 +43,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/genie_spaces/__init__.py b/python/databricks/bundles/genie_spaces/__init__.py index 960dd98014d..5dd9ef0f56e 100644 --- a/python/databricks/bundles/genie_spaces/__init__.py +++ b/python/databricks/bundles/genie_spaces/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -18,6 +21,7 @@ ] +from databricks.bundles.genie_spaces._models.bind import Bind, BindDict, BindParam from databricks.bundles.genie_spaces._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/genie_spaces/_models/bind.py b/python/databricks/bundles/genie_spaces/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/genie_spaces/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/genie_spaces/_models/genie_space.py b/python/databricks/bundles/genie_spaces/_models/genie_space.py index 6b7cd0532d4..bb1ec94c8df 100644 --- a/python/databricks/bundles/genie_spaces/_models/genie_space.py +++ b/python/databricks/bundles/genie_spaces/_models/genie_space.py @@ -7,10 +7,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional -from databricks.bundles.genie_spaces._models.lifecycle import ( - Lifecycle, - LifecycleParam, -) +from databricks.bundles.genie_spaces._models.lifecycle import Lifecycle, LifecycleParam from databricks.bundles.genie_spaces._models.permission import ( Permission, PermissionParam, diff --git a/python/databricks/bundles/genie_spaces/_models/lifecycle.py b/python/databricks/bundles/genie_spaces/_models/lifecycle.py index 8e740937885..8fb76376854 100644 --- a/python/databricks/bundles/genie_spaces/_models/lifecycle.py +++ b/python/databricks/bundles/genie_spaces/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.genie_spaces._models.bind import Bind, BindParam from databricks.bundles.genie_spaces._models.depends_on import DependsOn, DependsOnParam if TYPE_CHECKING: @@ -16,6 +17,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -37,6 +43,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/instance_pools/__init__.py b/python/databricks/bundles/instance_pools/__init__.py index 782be8340a5..8a842a373a5 100644 --- a/python/databricks/bundles/instance_pools/__init__.py +++ b/python/databricks/bundles/instance_pools/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -52,6 +55,7 @@ ] +from databricks.bundles.instance_pools._models.bind import Bind, BindDict, BindParam from databricks.bundles.instance_pools._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/instance_pools/_models/bind.py b/python/databricks/bundles/instance_pools/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/instance_pools/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/instance_pools/_models/lifecycle.py b/python/databricks/bundles/instance_pools/_models/lifecycle.py index 182c0682172..7378572c2a3 100644 --- a/python/databricks/bundles/instance_pools/_models/lifecycle.py +++ b/python/databricks/bundles/instance_pools/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.instance_pools._models.bind import Bind, BindParam from databricks.bundles.instance_pools._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +20,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +46,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/job_runs/__init__.py b/python/databricks/bundles/job_runs/__init__.py index 4b83903777d..9a71d0b3290 100644 --- a/python/databricks/bundles/job_runs/__init__.py +++ b/python/databricks/bundles/job_runs/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -24,6 +27,7 @@ ] +from databricks.bundles.job_runs._models.bind import Bind, BindDict, BindParam from databricks.bundles.job_runs._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/job_runs/_models/bind.py b/python/databricks/bundles/job_runs/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/job_runs/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/job_runs/_models/job_run_lifecycle.py b/python/databricks/bundles/job_runs/_models/job_run_lifecycle.py index 01d44e54ad3..7a012daddd5 100644 --- a/python/databricks/bundles/job_runs/_models/job_run_lifecycle.py +++ b/python/databricks/bundles/job_runs/_models/job_run_lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.job_runs._models.bind import Bind, BindParam from databricks.bundles.job_runs._models.depends_on import DependsOn, DependsOnParam from databricks.bundles.job_runs._models.job_run_trigger import ( JobRunTrigger, @@ -20,6 +21,11 @@ class JobRunLifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -46,6 +52,11 @@ def as_dict(self) -> "JobRunLifecycleDict": class JobRunLifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/jobs/__init__.py b/python/databricks/bundles/jobs/__init__.py index d75e2474de1..ef42f2f8dbe 100644 --- a/python/databricks/bundles/jobs/__init__.py +++ b/python/databricks/bundles/jobs/__init__.py @@ -52,6 +52,9 @@ "AzureAttributesParam", "AzureAvailability", "AzureAvailabilityParam", + "Bind", + "BindDict", + "BindParam", "CleanRoomsNotebookTask", "CleanRoomsNotebookTaskDict", "CleanRoomsNotebookTaskParam", @@ -494,6 +497,7 @@ AzureAvailability, AzureAvailabilityParam, ) +from databricks.bundles.jobs._models.bind import Bind, BindDict, BindParam from databricks.bundles.jobs._models.clean_rooms_notebook_task import ( CleanRoomsNotebookTask, CleanRoomsNotebookTaskDict, diff --git a/python/databricks/bundles/jobs/_models/bind.py b/python/databricks/bundles/jobs/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/jobs/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/jobs/_models/lifecycle.py b/python/databricks/bundles/jobs/_models/lifecycle.py index 254663c54d0..6124d357626 100644 --- a/python/databricks/bundles/jobs/_models/lifecycle.py +++ b/python/databricks/bundles/jobs/_models/lifecycle.py @@ -6,10 +6,8 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional -from databricks.bundles.jobs._models.depends_on import ( - DependsOn, - DependsOnParam, -) +from databricks.bundles.jobs._models.bind import Bind, BindParam +from databricks.bundles.jobs._models.depends_on import DependsOn, DependsOnParam if TYPE_CHECKING: from typing_extensions import Self @@ -19,6 +17,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +43,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/mcp_services/__init__.py b/python/databricks/bundles/mcp_services/__init__.py index 981e709bff8..af10858d3be 100644 --- a/python/databricks/bundles/mcp_services/__init__.py +++ b/python/databricks/bundles/mcp_services/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -31,6 +34,7 @@ ] +from databricks.bundles.mcp_services._models.bind import Bind, BindDict, BindParam from databricks.bundles.mcp_services._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/mcp_services/_models/bind.py b/python/databricks/bundles/mcp_services/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/mcp_services/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/mcp_services/_models/lifecycle.py b/python/databricks/bundles/mcp_services/_models/lifecycle.py index eca16a56d15..49ede7eecd0 100644 --- a/python/databricks/bundles/mcp_services/_models/lifecycle.py +++ b/python/databricks/bundles/mcp_services/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.mcp_services._models.bind import Bind, BindParam from databricks.bundles.mcp_services._models.depends_on import DependsOn, DependsOnParam if TYPE_CHECKING: @@ -16,6 +17,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -37,6 +43,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/mcp_services/_models/mcp_service.py b/python/databricks/bundles/mcp_services/_models/mcp_service.py index 129935ea097..1050e5153f4 100644 --- a/python/databricks/bundles/mcp_services/_models/mcp_service.py +++ b/python/databricks/bundles/mcp_services/_models/mcp_service.py @@ -11,7 +11,10 @@ VariableOrList, VariableOrOptional, ) -from databricks.bundles.mcp_services._models.lifecycle import Lifecycle, LifecycleParam +from databricks.bundles.mcp_services._models.lifecycle import ( + Lifecycle, + LifecycleParam, +) from databricks.bundles.mcp_services._models.mcp_service_config import ( McpServiceConfig, McpServiceConfigParam, diff --git a/python/databricks/bundles/model_provider_services/__init__.py b/python/databricks/bundles/model_provider_services/__init__.py index 57b9bb80463..ed93a148814 100644 --- a/python/databricks/bundles/model_provider_services/__init__.py +++ b/python/databricks/bundles/model_provider_services/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -102,6 +105,11 @@ ] +from databricks.bundles.model_provider_services._models.bind import ( + Bind, + BindDict, + BindParam, +) from databricks.bundles.model_provider_services._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/model_provider_services/_models/bind.py b/python/databricks/bundles/model_provider_services/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/model_provider_services/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/model_provider_services/_models/lifecycle.py b/python/databricks/bundles/model_provider_services/_models/lifecycle.py index 2837780438d..4ec7f31cd03 100644 --- a/python/databricks/bundles/model_provider_services/_models/lifecycle.py +++ b/python/databricks/bundles/model_provider_services/_models/lifecycle.py @@ -6,6 +6,10 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.model_provider_services._models.bind import ( + Bind, + BindParam, +) from databricks.bundles.model_provider_services._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +23,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +49,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/model_services/__init__.py b/python/databricks/bundles/model_services/__init__.py index 2e40ce88cb2..157f633fa01 100644 --- a/python/databricks/bundles/model_services/__init__.py +++ b/python/databricks/bundles/model_services/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -54,6 +57,7 @@ ] +from databricks.bundles.model_services._models.bind import Bind, BindDict, BindParam from databricks.bundles.model_services._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/model_services/_models/bind.py b/python/databricks/bundles/model_services/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/model_services/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/model_services/_models/lifecycle.py b/python/databricks/bundles/model_services/_models/lifecycle.py index fa15d53667c..55690e15c89 100644 --- a/python/databricks/bundles/model_services/_models/lifecycle.py +++ b/python/databricks/bundles/model_services/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.model_services._models.bind import Bind, BindParam from databricks.bundles.model_services._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +20,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +46,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/model_serving_endpoints/__init__.py b/python/databricks/bundles/model_serving_endpoints/__init__.py index be47098b53e..ef7fb7b1015 100644 --- a/python/databricks/bundles/model_serving_endpoints/__init__.py +++ b/python/databricks/bundles/model_serving_endpoints/__init__.py @@ -48,6 +48,9 @@ "BearerTokenAuth", "BearerTokenAuthDict", "BearerTokenAuthParam", + "Bind", + "BindDict", + "BindParam", "CohereConfig", "CohereConfigDict", "CohereConfigParam", @@ -215,6 +218,11 @@ BearerTokenAuthDict, BearerTokenAuthParam, ) +from databricks.bundles.model_serving_endpoints._models.bind import ( + Bind, + BindDict, + BindParam, +) from databricks.bundles.model_serving_endpoints._models.cohere_config import ( CohereConfig, CohereConfigDict, diff --git a/python/databricks/bundles/model_serving_endpoints/_models/bind.py b/python/databricks/bundles/model_serving_endpoints/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/model_serving_endpoints/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/model_serving_endpoints/_models/lifecycle.py b/python/databricks/bundles/model_serving_endpoints/_models/lifecycle.py index f6e3412eb9a..4690fd5eb02 100644 --- a/python/databricks/bundles/model_serving_endpoints/_models/lifecycle.py +++ b/python/databricks/bundles/model_serving_endpoints/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.model_serving_endpoints._models.bind import Bind, BindParam from databricks.bundles.model_serving_endpoints._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +20,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +46,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/models/__init__.py b/python/databricks/bundles/models/__init__.py index fa9922454b7..e040ceeb807 100644 --- a/python/databricks/bundles/models/__init__.py +++ b/python/databricks/bundles/models/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -21,6 +24,7 @@ ] +from databricks.bundles.models._models.bind import Bind, BindDict, BindParam from databricks.bundles.models._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/models/_models/bind.py b/python/databricks/bundles/models/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/models/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/models/_models/lifecycle.py b/python/databricks/bundles/models/_models/lifecycle.py index ed360157ba8..ae1b6916991 100644 --- a/python/databricks/bundles/models/_models/lifecycle.py +++ b/python/databricks/bundles/models/_models/lifecycle.py @@ -6,10 +6,8 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional -from databricks.bundles.models._models.depends_on import ( - DependsOn, - DependsOnParam, -) +from databricks.bundles.models._models.bind import Bind, BindParam +from databricks.bundles.models._models.depends_on import DependsOn, DependsOnParam if TYPE_CHECKING: from typing_extensions import Self @@ -19,6 +17,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +43,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/models/_models/mlflow_model.py b/python/databricks/bundles/models/_models/mlflow_model.py index 97c21186983..390f1155f2c 100644 --- a/python/databricks/bundles/models/_models/mlflow_model.py +++ b/python/databricks/bundles/models/_models/mlflow_model.py @@ -11,10 +11,7 @@ VariableOrList, VariableOrOptional, ) -from databricks.bundles.models._models.lifecycle import ( - Lifecycle, - LifecycleParam, -) +from databricks.bundles.models._models.lifecycle import Lifecycle, LifecycleParam from databricks.bundles.models._models.mlflow_model_permission import ( MlflowModelPermission, MlflowModelPermissionParam, diff --git a/python/databricks/bundles/pipelines/__init__.py b/python/databricks/bundles/pipelines/__init__.py index 412cbfa59b3..00f1b49a432 100644 --- a/python/databricks/bundles/pipelines/__init__.py +++ b/python/databricks/bundles/pipelines/__init__.py @@ -26,6 +26,9 @@ "AzureAttributesParam", "AzureAvailability", "AzureAvailabilityParam", + "Bind", + "BindDict", + "BindParam", "ClusterLogConf", "ClusterLogConfDict", "ClusterLogConfParam", @@ -338,6 +341,7 @@ AzureAvailability, AzureAvailabilityParam, ) +from databricks.bundles.pipelines._models.bind import Bind, BindDict, BindParam from databricks.bundles.pipelines._models.cluster_log_conf import ( ClusterLogConf, ClusterLogConfDict, diff --git a/python/databricks/bundles/pipelines/_models/bind.py b/python/databricks/bundles/pipelines/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/pipelines/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/pipelines/_models/lifecycle.py b/python/databricks/bundles/pipelines/_models/lifecycle.py index a8f228e0e0a..cdbc755571f 100644 --- a/python/databricks/bundles/pipelines/_models/lifecycle.py +++ b/python/databricks/bundles/pipelines/_models/lifecycle.py @@ -6,10 +6,8 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional -from databricks.bundles.pipelines._models.depends_on import ( - DependsOn, - DependsOnParam, -) +from databricks.bundles.pipelines._models.bind import Bind, BindParam +from databricks.bundles.pipelines._models.depends_on import DependsOn, DependsOnParam if TYPE_CHECKING: from typing_extensions import Self @@ -19,6 +17,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +43,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/pipelines/_models/pipeline.py b/python/databricks/bundles/pipelines/_models/pipeline.py index 070e813ef34..5e4b04bb726 100644 --- a/python/databricks/bundles/pipelines/_models/pipeline.py +++ b/python/databricks/bundles/pipelines/_models/pipeline.py @@ -27,10 +27,7 @@ IngestionPipelineDefinition, IngestionPipelineDefinitionParam, ) -from databricks.bundles.pipelines._models.lifecycle import ( - Lifecycle, - LifecycleParam, -) +from databricks.bundles.pipelines._models.lifecycle import Lifecycle, LifecycleParam from databricks.bundles.pipelines._models.notifications import ( Notifications, NotificationsParam, diff --git a/python/databricks/bundles/quality_monitors/__init__.py b/python/databricks/bundles/quality_monitors/__init__.py index 806df764ade..e281999b695 100644 --- a/python/databricks/bundles/quality_monitors/__init__.py +++ b/python/databricks/bundles/quality_monitors/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -43,6 +46,7 @@ ] +from databricks.bundles.quality_monitors._models.bind import Bind, BindDict, BindParam from databricks.bundles.quality_monitors._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/quality_monitors/_models/bind.py b/python/databricks/bundles/quality_monitors/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/quality_monitors/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/quality_monitors/_models/lifecycle.py b/python/databricks/bundles/quality_monitors/_models/lifecycle.py index 53ba2aafd3d..e7dafdb464e 100644 --- a/python/databricks/bundles/quality_monitors/_models/lifecycle.py +++ b/python/databricks/bundles/quality_monitors/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.quality_monitors._models.bind import Bind, BindParam from databricks.bundles.quality_monitors._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +20,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +46,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/registered_models/__init__.py b/python/databricks/bundles/registered_models/__init__.py index e193f063599..932c5bd5b4d 100644 --- a/python/databricks/bundles/registered_models/__init__.py +++ b/python/databricks/bundles/registered_models/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -21,6 +24,7 @@ ] +from databricks.bundles.registered_models._models.bind import Bind, BindDict, BindParam from databricks.bundles.registered_models._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/registered_models/_models/bind.py b/python/databricks/bundles/registered_models/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/registered_models/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/registered_models/_models/lifecycle.py b/python/databricks/bundles/registered_models/_models/lifecycle.py index b8642305131..9a42fa739e1 100644 --- a/python/databricks/bundles/registered_models/_models/lifecycle.py +++ b/python/databricks/bundles/registered_models/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.registered_models._models.bind import Bind, BindParam from databricks.bundles.registered_models._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +20,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +46,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/schemas/__init__.py b/python/databricks/bundles/schemas/__init__.py index 60ce398bd97..4ba569f4670 100644 --- a/python/databricks/bundles/schemas/__init__.py +++ b/python/databricks/bundles/schemas/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -23,6 +26,7 @@ ] +from databricks.bundles.schemas._models.bind import Bind, BindDict, BindParam from databricks.bundles.schemas._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/schemas/_models/bind.py b/python/databricks/bundles/schemas/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/schemas/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/schemas/_models/lifecycle.py b/python/databricks/bundles/schemas/_models/lifecycle.py index c8467b44428..d1fb0845e76 100644 --- a/python/databricks/bundles/schemas/_models/lifecycle.py +++ b/python/databricks/bundles/schemas/_models/lifecycle.py @@ -6,10 +6,8 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional -from databricks.bundles.schemas._models.depends_on import ( - DependsOn, - DependsOnParam, -) +from databricks.bundles.schemas._models.bind import Bind, BindParam +from databricks.bundles.schemas._models.depends_on import DependsOn, DependsOnParam if TYPE_CHECKING: from typing_extensions import Self @@ -19,6 +17,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +43,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/schemas/_models/schema.py b/python/databricks/bundles/schemas/_models/schema.py index eaa1f023cf9..32ff22c8227 100644 --- a/python/databricks/bundles/schemas/_models/schema.py +++ b/python/databricks/bundles/schemas/_models/schema.py @@ -12,10 +12,7 @@ VariableOrList, VariableOrOptional, ) -from databricks.bundles.schemas._models.lifecycle import ( - Lifecycle, - LifecycleParam, -) +from databricks.bundles.schemas._models.lifecycle import Lifecycle, LifecycleParam from databricks.bundles.schemas._models.privilege_assignment import ( PrivilegeAssignment, PrivilegeAssignmentParam, diff --git a/python/databricks/bundles/secret_scopes/__init__.py b/python/databricks/bundles/secret_scopes/__init__.py index d85f347bbba..9a6109b9375 100644 --- a/python/databricks/bundles/secret_scopes/__init__.py +++ b/python/databricks/bundles/secret_scopes/__init__.py @@ -4,6 +4,9 @@ "AzureKeyVaultSecretScopeMetadata", "AzureKeyVaultSecretScopeMetadataDict", "AzureKeyVaultSecretScopeMetadataParam", + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -28,6 +31,7 @@ AzureKeyVaultSecretScopeMetadataDict, AzureKeyVaultSecretScopeMetadataParam, ) +from databricks.bundles.secret_scopes._models.bind import Bind, BindDict, BindParam from databricks.bundles.secret_scopes._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/secret_scopes/_models/bind.py b/python/databricks/bundles/secret_scopes/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/secret_scopes/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/secret_scopes/_models/lifecycle.py b/python/databricks/bundles/secret_scopes/_models/lifecycle.py index 7f1b17140e0..0e7190c3414 100644 --- a/python/databricks/bundles/secret_scopes/_models/lifecycle.py +++ b/python/databricks/bundles/secret_scopes/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.secret_scopes._models.bind import Bind, BindParam from databricks.bundles.secret_scopes._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +20,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +46,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/secret_scopes/_models/secret_scope.py b/python/databricks/bundles/secret_scopes/_models/secret_scope.py index 022aaafb82d..5e436f1d247 100644 --- a/python/databricks/bundles/secret_scopes/_models/secret_scope.py +++ b/python/databricks/bundles/secret_scopes/_models/secret_scope.py @@ -15,10 +15,7 @@ AzureKeyVaultSecretScopeMetadata, AzureKeyVaultSecretScopeMetadataParam, ) -from databricks.bundles.secret_scopes._models.lifecycle import ( - Lifecycle, - LifecycleParam, -) +from databricks.bundles.secret_scopes._models.lifecycle import Lifecycle, LifecycleParam from databricks.bundles.secret_scopes._models.scope_backend_type import ( ScopeBackendType, ScopeBackendTypeParam, diff --git a/python/databricks/bundles/secrets/__init__.py b/python/databricks/bundles/secrets/__init__.py index 138d56de11b..5698fdad304 100644 --- a/python/databricks/bundles/secrets/__init__.py +++ b/python/databricks/bundles/secrets/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -18,6 +21,7 @@ ] +from databricks.bundles.secrets._models.bind import Bind, BindDict, BindParam from databricks.bundles.secrets._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/secrets/_models/bind.py b/python/databricks/bundles/secrets/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/secrets/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/secrets/_models/lifecycle.py b/python/databricks/bundles/secrets/_models/lifecycle.py index 1bd7bc1189a..25247266ed5 100644 --- a/python/databricks/bundles/secrets/_models/lifecycle.py +++ b/python/databricks/bundles/secrets/_models/lifecycle.py @@ -6,10 +6,8 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional -from databricks.bundles.secrets._models.depends_on import ( - DependsOn, - DependsOnParam, -) +from databricks.bundles.secrets._models.bind import Bind, BindParam +from databricks.bundles.secrets._models.depends_on import DependsOn, DependsOnParam if TYPE_CHECKING: from typing_extensions import Self @@ -19,6 +17,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +43,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/secrets/_models/secret.py b/python/databricks/bundles/secrets/_models/secret.py index d6af914a045..b0db4c03ada 100644 --- a/python/databricks/bundles/secrets/_models/secret.py +++ b/python/databricks/bundles/secrets/_models/secret.py @@ -11,10 +11,7 @@ VariableOrList, VariableOrOptional, ) -from databricks.bundles.secrets._models.lifecycle import ( - Lifecycle, - LifecycleParam, -) +from databricks.bundles.secrets._models.lifecycle import Lifecycle, LifecycleParam from databricks.bundles.secrets._models.privilege_assignment import ( PrivilegeAssignment, PrivilegeAssignmentParam, diff --git a/python/databricks/bundles/sql_warehouses/__init__.py b/python/databricks/bundles/sql_warehouses/__init__.py index 3f19eb82205..375f91f8b5d 100644 --- a/python/databricks/bundles/sql_warehouses/__init__.py +++ b/python/databricks/bundles/sql_warehouses/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "Channel", "ChannelDict", "ChannelName", @@ -33,6 +36,7 @@ ] +from databricks.bundles.sql_warehouses._models.bind import Bind, BindDict, BindParam from databricks.bundles.sql_warehouses._models.channel import ( Channel, ChannelDict, diff --git a/python/databricks/bundles/sql_warehouses/_models/bind.py b/python/databricks/bundles/sql_warehouses/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/sql_warehouses/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/sql_warehouses/_models/lifecycle_with_started.py b/python/databricks/bundles/sql_warehouses/_models/lifecycle_with_started.py index 54f0d97ae5a..c62c481c96b 100644 --- a/python/databricks/bundles/sql_warehouses/_models/lifecycle_with_started.py +++ b/python/databricks/bundles/sql_warehouses/_models/lifecycle_with_started.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.sql_warehouses._models.bind import Bind, BindParam from databricks.bundles.sql_warehouses._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +20,11 @@ class LifecycleWithStarted: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -45,6 +51,11 @@ def as_dict(self) -> "LifecycleWithStartedDict": class LifecycleWithStartedDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/synced_database_tables/__init__.py b/python/databricks/bundles/synced_database_tables/__init__.py index c626e16396c..dfa5563d2c0 100644 --- a/python/databricks/bundles/synced_database_tables/__init__.py +++ b/python/databricks/bundles/synced_database_tables/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -26,6 +29,11 @@ ] +from databricks.bundles.synced_database_tables._models.bind import ( + Bind, + BindDict, + BindParam, +) from databricks.bundles.synced_database_tables._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/synced_database_tables/_models/bind.py b/python/databricks/bundles/synced_database_tables/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/synced_database_tables/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/synced_database_tables/_models/lifecycle.py b/python/databricks/bundles/synced_database_tables/_models/lifecycle.py index 6145aa8ac96..dc61dd1ea9d 100644 --- a/python/databricks/bundles/synced_database_tables/_models/lifecycle.py +++ b/python/databricks/bundles/synced_database_tables/_models/lifecycle.py @@ -6,6 +6,10 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.synced_database_tables._models.bind import ( + Bind, + BindParam, +) from databricks.bundles.synced_database_tables._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +23,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +49,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/vector_search_endpoints/__init__.py b/python/databricks/bundles/vector_search_endpoints/__init__.py index ad58a72b996..c33e2dcd6af 100644 --- a/python/databricks/bundles/vector_search_endpoints/__init__.py +++ b/python/databricks/bundles/vector_search_endpoints/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -20,6 +23,11 @@ ] +from databricks.bundles.vector_search_endpoints._models.bind import ( + Bind, + BindDict, + BindParam, +) from databricks.bundles.vector_search_endpoints._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/vector_search_endpoints/_models/bind.py b/python/databricks/bundles/vector_search_endpoints/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/vector_search_endpoints/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/vector_search_endpoints/_models/lifecycle.py b/python/databricks/bundles/vector_search_endpoints/_models/lifecycle.py index 45f1d1427b3..2f54e82b59a 100644 --- a/python/databricks/bundles/vector_search_endpoints/_models/lifecycle.py +++ b/python/databricks/bundles/vector_search_endpoints/_models/lifecycle.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.vector_search_endpoints._models.bind import Bind, BindParam from databricks.bundles.vector_search_endpoints._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +20,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +46,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/vector_search_indexes/__init__.py b/python/databricks/bundles/vector_search_indexes/__init__.py index 8a23b46be42..bcf11f12f55 100644 --- a/python/databricks/bundles/vector_search_indexes/__init__.py +++ b/python/databricks/bundles/vector_search_indexes/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DeltaSyncVectorIndexSpecRequest", "DeltaSyncVectorIndexSpecRequestDict", "DeltaSyncVectorIndexSpecRequestParam", @@ -36,6 +39,11 @@ ] +from databricks.bundles.vector_search_indexes._models.bind import ( + Bind, + BindDict, + BindParam, +) from databricks.bundles.vector_search_indexes._models.delta_sync_vector_index_spec_request import ( DeltaSyncVectorIndexSpecRequest, DeltaSyncVectorIndexSpecRequestDict, diff --git a/python/databricks/bundles/vector_search_indexes/_models/bind.py b/python/databricks/bundles/vector_search_indexes/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/vector_search_indexes/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/vector_search_indexes/_models/lifecycle.py b/python/databricks/bundles/vector_search_indexes/_models/lifecycle.py index b03f75d8f6e..efc70d6ac85 100644 --- a/python/databricks/bundles/vector_search_indexes/_models/lifecycle.py +++ b/python/databricks/bundles/vector_search_indexes/_models/lifecycle.py @@ -6,6 +6,10 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.vector_search_indexes._models.bind import ( + Bind, + BindParam, +) from databricks.bundles.vector_search_indexes._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +23,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +49,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/volumes/__init__.py b/python/databricks/bundles/volumes/__init__.py index 3fdd871e124..3432bbb9a1f 100644 --- a/python/databricks/bundles/volumes/__init__.py +++ b/python/databricks/bundles/volumes/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "DependsOn", "DependsOnDict", "DependsOnParam", @@ -25,6 +28,7 @@ ] +from databricks.bundles.volumes._models.bind import Bind, BindDict, BindParam from databricks.bundles.volumes._models.depends_on import ( DependsOn, DependsOnDict, diff --git a/python/databricks/bundles/volumes/_models/bind.py b/python/databricks/bundles/volumes/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/volumes/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/volumes/_models/lifecycle.py b/python/databricks/bundles/volumes/_models/lifecycle.py index 818b3d25108..c7013db6ce7 100644 --- a/python/databricks/bundles/volumes/_models/lifecycle.py +++ b/python/databricks/bundles/volumes/_models/lifecycle.py @@ -6,10 +6,8 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional -from databricks.bundles.volumes._models.depends_on import ( - DependsOn, - DependsOnParam, -) +from databricks.bundles.volumes._models.bind import Bind, BindParam +from databricks.bundles.volumes._models.depends_on import DependsOn, DependsOnParam if TYPE_CHECKING: from typing_extensions import Self @@ -19,6 +17,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +43,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/volumes/_models/volume.py b/python/databricks/bundles/volumes/_models/volume.py index fb6f2462d0c..db248fbba94 100644 --- a/python/databricks/bundles/volumes/_models/volume.py +++ b/python/databricks/bundles/volumes/_models/volume.py @@ -11,10 +11,7 @@ VariableOrList, VariableOrOptional, ) -from databricks.bundles.volumes._models.lifecycle import ( - Lifecycle, - LifecycleParam, -) +from databricks.bundles.volumes._models.lifecycle import Lifecycle, LifecycleParam from databricks.bundles.volumes._models.privilege_assignment import ( PrivilegeAssignment, PrivilegeAssignmentParam, From 89af9adc395fe12973192a3896d25e2da1cb3d2e Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 17:04:17 +0000 Subject: [PATCH 15/20] Regenerate the reference schema outputs for lifecycle.bind Co-authored-by: Isaac --- acceptance/bundle/refschema/output/alerts.txt | 2 ++ acceptance/bundle/refschema/output/apps.txt | 2 ++ acceptance/bundle/refschema/output/catalogs.txt | 2 ++ acceptance/bundle/refschema/output/cluster_policies.txt | 2 ++ acceptance/bundle/refschema/output/clusters.txt | 2 ++ acceptance/bundle/refschema/output/dashboards.txt | 2 ++ acceptance/bundle/refschema/output/database_catalogs.txt | 2 ++ acceptance/bundle/refschema/output/database_instances.txt | 2 ++ acceptance/bundle/refschema/output/experiments.txt | 2 ++ acceptance/bundle/refschema/output/external_locations.txt | 2 ++ acceptance/bundle/refschema/output/features.txt | 2 ++ acceptance/bundle/refschema/output/genie_spaces.txt | 2 ++ acceptance/bundle/refschema/output/instance_pools.txt | 2 ++ acceptance/bundle/refschema/output/job_runs.txt | 2 ++ acceptance/bundle/refschema/output/jobs.txt | 2 ++ acceptance/bundle/refschema/output/mcp_services.txt | 2 ++ acceptance/bundle/refschema/output/model_provider_services.txt | 2 ++ acceptance/bundle/refschema/output/model_services.txt | 2 ++ acceptance/bundle/refschema/output/model_serving_endpoints.txt | 2 ++ acceptance/bundle/refschema/output/models.txt | 2 ++ acceptance/bundle/refschema/output/pipelines.txt | 2 ++ acceptance/bundle/refschema/output/postgres_branches.txt | 2 ++ acceptance/bundle/refschema/output/postgres_catalogs.txt | 2 ++ acceptance/bundle/refschema/output/postgres_databases.txt | 2 ++ acceptance/bundle/refschema/output/postgres_endpoints.txt | 2 ++ acceptance/bundle/refschema/output/postgres_projects.txt | 2 ++ acceptance/bundle/refschema/output/postgres_roles.txt | 2 ++ .../bundle/refschema/output/postgres_snapshot_schedules.txt | 2 ++ acceptance/bundle/refschema/output/postgres_synced_tables.txt | 2 ++ acceptance/bundle/refschema/output/quality_monitors.txt | 2 ++ acceptance/bundle/refschema/output/registered_models.txt | 2 ++ acceptance/bundle/refschema/output/schemas.txt | 2 ++ acceptance/bundle/refschema/output/secret_scopes.txt | 2 ++ acceptance/bundle/refschema/output/secrets.txt | 2 ++ acceptance/bundle/refschema/output/sql_warehouses.txt | 2 ++ acceptance/bundle/refschema/output/synced_database_tables.txt | 2 ++ acceptance/bundle/refschema/output/vector_search_endpoints.txt | 2 ++ acceptance/bundle/refschema/output/vector_search_indexes.txt | 2 ++ acceptance/bundle/refschema/output/volumes.txt | 2 ++ 39 files changed, 78 insertions(+) diff --git a/acceptance/bundle/refschema/output/alerts.txt b/acceptance/bundle/refschema/output/alerts.txt index 566299e11cd..518dcb5369c 100644 --- a/acceptance/bundle/refschema/output/alerts.txt +++ b/acceptance/bundle/refschema/output/alerts.txt @@ -34,6 +34,8 @@ resources.alerts.*.evaluation.threshold.value.string_value string ALL resources.alerts.*.file_path string INPUT resources.alerts.*.id string ALL resources.alerts.*.lifecycle resources.Lifecycle INPUT +resources.alerts.*.lifecycle.bind *resources.Bind INPUT +resources.alerts.*.lifecycle.bind.id string INPUT resources.alerts.*.lifecycle.depends_on []resources.DependsOn INPUT resources.alerts.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.alerts.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/apps.txt b/acceptance/bundle/refschema/output/apps.txt index 3e1f91151f1..3087fafd5c0 100644 --- a/acceptance/bundle/refschema/output/apps.txt +++ b/acceptance/bundle/refschema/output/apps.txt @@ -91,6 +91,8 @@ resources.apps.*.id string ALL resources.apps.*.lifecycle *dresources.StateLifecycle REMOTE STATE resources.apps.*.lifecycle *resources.LifecycleWithStarted INPUT resources.apps.*.lifecycle resources.Lifecycle INPUT +resources.apps.*.lifecycle.bind *resources.Bind INPUT +resources.apps.*.lifecycle.bind.id string INPUT resources.apps.*.lifecycle.depends_on []resources.DependsOn INPUT resources.apps.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.apps.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/catalogs.txt b/acceptance/bundle/refschema/output/catalogs.txt index feaee00d2ca..e2c0a061582 100644 --- a/acceptance/bundle/refschema/output/catalogs.txt +++ b/acceptance/bundle/refschema/output/catalogs.txt @@ -14,6 +14,8 @@ resources.catalogs.*.full_name string REMOTE resources.catalogs.*.id string INPUT resources.catalogs.*.isolation_mode catalog.CatalogIsolationMode REMOTE resources.catalogs.*.lifecycle resources.Lifecycle INPUT +resources.catalogs.*.lifecycle.bind *resources.Bind INPUT +resources.catalogs.*.lifecycle.bind.id string INPUT resources.catalogs.*.lifecycle.depends_on []resources.DependsOn INPUT resources.catalogs.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.catalogs.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/cluster_policies.txt b/acceptance/bundle/refschema/output/cluster_policies.txt index da5be2b6b9b..bb8f08ad455 100644 --- a/acceptance/bundle/refschema/output/cluster_policies.txt +++ b/acceptance/bundle/refschema/output/cluster_policies.txt @@ -23,6 +23,8 @@ resources.cluster_policies.*.libraries[*].pypi.repo string ALL resources.cluster_policies.*.libraries[*].requirements string ALL resources.cluster_policies.*.libraries[*].whl string ALL resources.cluster_policies.*.lifecycle resources.Lifecycle INPUT +resources.cluster_policies.*.lifecycle.bind *resources.Bind INPUT +resources.cluster_policies.*.lifecycle.bind.id string INPUT resources.cluster_policies.*.lifecycle.depends_on []resources.DependsOn INPUT resources.cluster_policies.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.cluster_policies.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/clusters.txt b/acceptance/bundle/refschema/output/clusters.txt index ad9c1e333a5..e672cb77771 100644 --- a/acceptance/bundle/refschema/output/clusters.txt +++ b/acceptance/bundle/refschema/output/clusters.txt @@ -140,6 +140,8 @@ resources.clusters.*.libraries[*].whl string ALL resources.clusters.*.lifecycle *dresources.StateLifecycle REMOTE STATE resources.clusters.*.lifecycle *resources.LifecycleWithStarted INPUT resources.clusters.*.lifecycle resources.Lifecycle INPUT +resources.clusters.*.lifecycle.bind *resources.Bind INPUT +resources.clusters.*.lifecycle.bind.id string INPUT resources.clusters.*.lifecycle.depends_on []resources.DependsOn INPUT resources.clusters.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.clusters.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/dashboards.txt b/acceptance/bundle/refschema/output/dashboards.txt index 300a7d9c3a7..6a5a1356938 100644 --- a/acceptance/bundle/refschema/output/dashboards.txt +++ b/acceptance/bundle/refschema/output/dashboards.txt @@ -8,6 +8,8 @@ resources.dashboards.*.etag string ALL resources.dashboards.*.file_path string INPUT resources.dashboards.*.id string INPUT resources.dashboards.*.lifecycle resources.Lifecycle INPUT +resources.dashboards.*.lifecycle.bind *resources.Bind INPUT +resources.dashboards.*.lifecycle.bind.id string INPUT resources.dashboards.*.lifecycle.depends_on []resources.DependsOn INPUT resources.dashboards.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.dashboards.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/database_catalogs.txt b/acceptance/bundle/refschema/output/database_catalogs.txt index e23c5246e31..f36b1a79eb5 100644 --- a/acceptance/bundle/refschema/output/database_catalogs.txt +++ b/acceptance/bundle/refschema/output/database_catalogs.txt @@ -3,6 +3,8 @@ resources.database_catalogs.*.database_instance_name string ALL resources.database_catalogs.*.database_name string ALL resources.database_catalogs.*.id string INPUT resources.database_catalogs.*.lifecycle resources.Lifecycle INPUT +resources.database_catalogs.*.lifecycle.bind *resources.Bind INPUT +resources.database_catalogs.*.lifecycle.bind.id string INPUT resources.database_catalogs.*.lifecycle.depends_on []resources.DependsOn INPUT resources.database_catalogs.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.database_catalogs.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/database_instances.txt b/acceptance/bundle/refschema/output/database_instances.txt index d09ebf65dc2..ad4308b7245 100644 --- a/acceptance/bundle/refschema/output/database_instances.txt +++ b/acceptance/bundle/refschema/output/database_instances.txt @@ -27,6 +27,8 @@ resources.database_instances.*.enable_pg_native_login bool ALL resources.database_instances.*.enable_readable_secondaries bool ALL resources.database_instances.*.id string INPUT resources.database_instances.*.lifecycle resources.Lifecycle INPUT +resources.database_instances.*.lifecycle.bind *resources.Bind INPUT +resources.database_instances.*.lifecycle.bind.id string INPUT resources.database_instances.*.lifecycle.depends_on []resources.DependsOn INPUT resources.database_instances.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.database_instances.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/experiments.txt b/acceptance/bundle/refschema/output/experiments.txt index a85aafdd5d0..85384fd3e5f 100644 --- a/acceptance/bundle/refschema/output/experiments.txt +++ b/acceptance/bundle/refschema/output/experiments.txt @@ -4,6 +4,8 @@ resources.experiments.*.experiment_id string REMOTE resources.experiments.*.id string INPUT resources.experiments.*.last_update_time int64 REMOTE resources.experiments.*.lifecycle resources.Lifecycle INPUT +resources.experiments.*.lifecycle.bind *resources.Bind INPUT +resources.experiments.*.lifecycle.bind.id string INPUT resources.experiments.*.lifecycle.depends_on []resources.DependsOn INPUT resources.experiments.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.experiments.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/external_locations.txt b/acceptance/bundle/refschema/output/external_locations.txt index 4e058354db2..5950751b3e8 100644 --- a/acceptance/bundle/refschema/output/external_locations.txt +++ b/acceptance/bundle/refschema/output/external_locations.txt @@ -60,6 +60,8 @@ resources.external_locations.*.file_event_queue.provided_sqs.queue_url string AL resources.external_locations.*.id string INPUT resources.external_locations.*.isolation_mode catalog.IsolationMode REMOTE resources.external_locations.*.lifecycle resources.Lifecycle INPUT +resources.external_locations.*.lifecycle.bind *resources.Bind INPUT +resources.external_locations.*.lifecycle.bind.id string INPUT resources.external_locations.*.lifecycle.depends_on []resources.DependsOn INPUT resources.external_locations.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.external_locations.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/features.txt b/acceptance/bundle/refschema/output/features.txt index c06246cee3d..907f26be1b2 100644 --- a/acceptance/bundle/refschema/output/features.txt +++ b/acceptance/bundle/refschema/output/features.txt @@ -89,6 +89,8 @@ resources.features.*.id string INPUT resources.features.*.inputs []string ALL resources.features.*.inputs[*] string ALL resources.features.*.lifecycle resources.Lifecycle INPUT +resources.features.*.lifecycle.bind *resources.Bind INPUT +resources.features.*.lifecycle.bind.id string INPUT resources.features.*.lifecycle.depends_on []resources.DependsOn INPUT resources.features.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.features.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/genie_spaces.txt b/acceptance/bundle/refschema/output/genie_spaces.txt index 40dbd2b70ba..2bad6604528 100644 --- a/acceptance/bundle/refschema/output/genie_spaces.txt +++ b/acceptance/bundle/refschema/output/genie_spaces.txt @@ -3,6 +3,8 @@ resources.genie_spaces.*.etag string ALL resources.genie_spaces.*.file_path string INPUT resources.genie_spaces.*.id string INPUT resources.genie_spaces.*.lifecycle resources.Lifecycle INPUT +resources.genie_spaces.*.lifecycle.bind *resources.Bind INPUT +resources.genie_spaces.*.lifecycle.bind.id string INPUT resources.genie_spaces.*.lifecycle.depends_on []resources.DependsOn INPUT resources.genie_spaces.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.genie_spaces.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/instance_pools.txt b/acceptance/bundle/refschema/output/instance_pools.txt index 71991cd0733..8c7263f8a3e 100644 --- a/acceptance/bundle/refschema/output/instance_pools.txt +++ b/acceptance/bundle/refschema/output/instance_pools.txt @@ -29,6 +29,8 @@ resources.instance_pools.*.idle_instance_autotermination_minutes int ALL resources.instance_pools.*.instance_pool_id string REMOTE resources.instance_pools.*.instance_pool_name string ALL resources.instance_pools.*.lifecycle resources.Lifecycle INPUT +resources.instance_pools.*.lifecycle.bind *resources.Bind INPUT +resources.instance_pools.*.lifecycle.bind.id string INPUT resources.instance_pools.*.lifecycle.depends_on []resources.DependsOn INPUT resources.instance_pools.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.instance_pools.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/job_runs.txt b/acceptance/bundle/refschema/output/job_runs.txt index c613f750f7b..406b7093897 100644 --- a/acceptance/bundle/refschema/output/job_runs.txt +++ b/acceptance/bundle/refschema/output/job_runs.txt @@ -11,6 +11,8 @@ resources.job_runs.*.job_parameters.* string ALL resources.job_runs.*.lifecycle *dresources.JobRunLifecycleState STATE resources.job_runs.*.lifecycle *resources.JobRunLifecycle INPUT resources.job_runs.*.lifecycle resources.Lifecycle INPUT +resources.job_runs.*.lifecycle.bind *resources.Bind INPUT +resources.job_runs.*.lifecycle.bind.id string INPUT resources.job_runs.*.lifecycle.depends_on []resources.DependsOn INPUT resources.job_runs.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.job_runs.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/jobs.txt b/acceptance/bundle/refschema/output/jobs.txt index 06a058987fd..099ceeb7df9 100644 --- a/acceptance/bundle/refschema/output/jobs.txt +++ b/acceptance/bundle/refschema/output/jobs.txt @@ -198,6 +198,8 @@ resources.jobs.*.job_clusters[*].serverless_compute.policy_id string ALL resources.jobs.*.job_clusters[*].serverless_compute_id string ALL resources.jobs.*.job_id int64 REMOTE resources.jobs.*.lifecycle resources.Lifecycle INPUT +resources.jobs.*.lifecycle.bind *resources.Bind INPUT +resources.jobs.*.lifecycle.bind.id string INPUT resources.jobs.*.lifecycle.depends_on []resources.DependsOn INPUT resources.jobs.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.jobs.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/mcp_services.txt b/acceptance/bundle/refschema/output/mcp_services.txt index 578b2636d79..403db38c3d9 100644 --- a/acceptance/bundle/refschema/output/mcp_services.txt +++ b/acceptance/bundle/refschema/output/mcp_services.txt @@ -16,6 +16,8 @@ resources.mcp_services.*.config.source_connection.options map[string]string ALL resources.mcp_services.*.config.source_connection.options.* string ALL resources.mcp_services.*.id string INPUT resources.mcp_services.*.lifecycle resources.Lifecycle INPUT +resources.mcp_services.*.lifecycle.bind *resources.Bind INPUT +resources.mcp_services.*.lifecycle.bind.id string INPUT resources.mcp_services.*.lifecycle.depends_on []resources.DependsOn INPUT resources.mcp_services.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.mcp_services.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/model_provider_services.txt b/acceptance/bundle/refschema/output/model_provider_services.txt index 0d5e76e1a7f..c228aa54904 100644 --- a/acceptance/bundle/refschema/output/model_provider_services.txt +++ b/acceptance/bundle/refschema/output/model_provider_services.txt @@ -119,6 +119,8 @@ resources.model_provider_services.*.config.targets[*].native_api_types []string resources.model_provider_services.*.config.targets[*].native_api_types[*] string ALL resources.model_provider_services.*.id string INPUT resources.model_provider_services.*.lifecycle resources.Lifecycle INPUT +resources.model_provider_services.*.lifecycle.bind *resources.Bind INPUT +resources.model_provider_services.*.lifecycle.bind.id string INPUT resources.model_provider_services.*.lifecycle.depends_on []resources.DependsOn INPUT resources.model_provider_services.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.model_provider_services.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/model_services.txt b/acceptance/bundle/refschema/output/model_services.txt index a13287e468b..8a43df8baa8 100644 --- a/acceptance/bundle/refschema/output/model_services.txt +++ b/acceptance/bundle/refschema/output/model_services.txt @@ -52,6 +52,8 @@ resources.model_services.*.config.routing.fallback.destinations[*].provisioned_t resources.model_services.*.config.routing.fallback.destinations[*].traffic_percentage int ALL resources.model_services.*.id string INPUT resources.model_services.*.lifecycle resources.Lifecycle INPUT +resources.model_services.*.lifecycle.bind *resources.Bind INPUT +resources.model_services.*.lifecycle.bind.id string INPUT resources.model_services.*.lifecycle.depends_on []resources.DependsOn INPUT resources.model_services.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.model_services.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/model_serving_endpoints.txt b/acceptance/bundle/refschema/output/model_serving_endpoints.txt index 95a3adcf440..796a84a3d7d 100644 --- a/acceptance/bundle/refschema/output/model_serving_endpoints.txt +++ b/acceptance/bundle/refschema/output/model_serving_endpoints.txt @@ -461,6 +461,8 @@ resources.model_serving_endpoints.*.endpoint_details.telemetry_config.telemetry_ resources.model_serving_endpoints.*.endpoint_id string REMOTE resources.model_serving_endpoints.*.id string INPUT resources.model_serving_endpoints.*.lifecycle resources.Lifecycle INPUT +resources.model_serving_endpoints.*.lifecycle.bind *resources.Bind INPUT +resources.model_serving_endpoints.*.lifecycle.bind.id string INPUT resources.model_serving_endpoints.*.lifecycle.depends_on []resources.DependsOn INPUT resources.model_serving_endpoints.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.model_serving_endpoints.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/models.txt b/acceptance/bundle/refschema/output/models.txt index 3570dd6fb58..7886abda59d 100644 --- a/acceptance/bundle/refschema/output/models.txt +++ b/acceptance/bundle/refschema/output/models.txt @@ -21,6 +21,8 @@ resources.models.*.latest_versions[*].tags[*].value string REMOTE resources.models.*.latest_versions[*].user_id string REMOTE resources.models.*.latest_versions[*].version string REMOTE resources.models.*.lifecycle resources.Lifecycle INPUT +resources.models.*.lifecycle.bind *resources.Bind INPUT +resources.models.*.lifecycle.bind.id string INPUT resources.models.*.lifecycle.depends_on []resources.DependsOn INPUT resources.models.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.models.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/pipelines.txt b/acceptance/bundle/refschema/output/pipelines.txt index c8bc57f512c..e15ec9f418a 100644 --- a/acceptance/bundle/refschema/output/pipelines.txt +++ b/acceptance/bundle/refschema/output/pipelines.txt @@ -831,6 +831,8 @@ resources.pipelines.*.libraries[*].notebook *pipelines.NotebookLibrary ALL resources.pipelines.*.libraries[*].notebook.path string ALL resources.pipelines.*.libraries[*].whl string ALL resources.pipelines.*.lifecycle resources.Lifecycle INPUT +resources.pipelines.*.lifecycle.bind *resources.Bind INPUT +resources.pipelines.*.lifecycle.bind.id string INPUT resources.pipelines.*.lifecycle.depends_on []resources.DependsOn INPUT resources.pipelines.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.pipelines.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/postgres_branches.txt b/acceptance/bundle/refschema/output/postgres_branches.txt index 85248e7a93a..e7db507827a 100644 --- a/acceptance/bundle/refschema/output/postgres_branches.txt +++ b/acceptance/bundle/refschema/output/postgres_branches.txt @@ -4,6 +4,8 @@ resources.postgres_branches.*.expire_time *time.Time ALL resources.postgres_branches.*.id string INPUT resources.postgres_branches.*.is_protected bool ALL resources.postgres_branches.*.lifecycle resources.Lifecycle INPUT +resources.postgres_branches.*.lifecycle.bind *resources.Bind INPUT +resources.postgres_branches.*.lifecycle.bind.id string INPUT resources.postgres_branches.*.lifecycle.depends_on []resources.DependsOn INPUT resources.postgres_branches.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.postgres_branches.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/postgres_catalogs.txt b/acceptance/bundle/refschema/output/postgres_catalogs.txt index 3cefc681b99..30f13150c6f 100644 --- a/acceptance/bundle/refschema/output/postgres_catalogs.txt +++ b/acceptance/bundle/refschema/output/postgres_catalogs.txt @@ -4,6 +4,8 @@ resources.postgres_catalogs.*.create_database_if_missing bool ALL resources.postgres_catalogs.*.create_time *time.Time REMOTE resources.postgres_catalogs.*.id string INPUT resources.postgres_catalogs.*.lifecycle resources.Lifecycle INPUT +resources.postgres_catalogs.*.lifecycle.bind *resources.Bind INPUT +resources.postgres_catalogs.*.lifecycle.bind.id string INPUT resources.postgres_catalogs.*.lifecycle.depends_on []resources.DependsOn INPUT resources.postgres_catalogs.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.postgres_catalogs.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/postgres_databases.txt b/acceptance/bundle/refschema/output/postgres_databases.txt index 2ccb3ba2b68..505bc9cf551 100644 --- a/acceptance/bundle/refschema/output/postgres_databases.txt +++ b/acceptance/bundle/refschema/output/postgres_databases.txt @@ -2,6 +2,8 @@ resources.postgres_databases.*.create_time *time.Time REMOTE resources.postgres_databases.*.database_id string ALL resources.postgres_databases.*.id string INPUT resources.postgres_databases.*.lifecycle resources.Lifecycle INPUT +resources.postgres_databases.*.lifecycle.bind *resources.Bind INPUT +resources.postgres_databases.*.lifecycle.bind.id string INPUT resources.postgres_databases.*.lifecycle.depends_on []resources.DependsOn INPUT resources.postgres_databases.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.postgres_databases.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/postgres_endpoints.txt b/acceptance/bundle/refschema/output/postgres_endpoints.txt index 32f2395228b..ab1280898bf 100644 --- a/acceptance/bundle/refschema/output/postgres_endpoints.txt +++ b/acceptance/bundle/refschema/output/postgres_endpoints.txt @@ -10,6 +10,8 @@ resources.postgres_endpoints.*.group.max int ALL resources.postgres_endpoints.*.group.min int ALL resources.postgres_endpoints.*.id string INPUT resources.postgres_endpoints.*.lifecycle resources.Lifecycle INPUT +resources.postgres_endpoints.*.lifecycle.bind *resources.Bind INPUT +resources.postgres_endpoints.*.lifecycle.bind.id string INPUT resources.postgres_endpoints.*.lifecycle.depends_on []resources.DependsOn INPUT resources.postgres_endpoints.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.postgres_endpoints.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/postgres_projects.txt b/acceptance/bundle/refschema/output/postgres_projects.txt index 575b12db52d..f9727d43730 100644 --- a/acceptance/bundle/refschema/output/postgres_projects.txt +++ b/acceptance/bundle/refschema/output/postgres_projects.txt @@ -27,6 +27,8 @@ resources.postgres_projects.*.initial_endpoint_spec.group.min int REMOTE resources.postgres_projects.*.initial_endpoint_spec.no_suspension bool REMOTE resources.postgres_projects.*.initial_endpoint_spec.suspend_timeout_duration *duration.Duration REMOTE resources.postgres_projects.*.lifecycle resources.Lifecycle INPUT +resources.postgres_projects.*.lifecycle.bind *resources.Bind INPUT +resources.postgres_projects.*.lifecycle.bind.id string INPUT resources.postgres_projects.*.lifecycle.depends_on []resources.DependsOn INPUT resources.postgres_projects.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.postgres_projects.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/postgres_roles.txt b/acceptance/bundle/refschema/output/postgres_roles.txt index b77c3258488..cba2e3ae34d 100644 --- a/acceptance/bundle/refschema/output/postgres_roles.txt +++ b/acceptance/bundle/refschema/output/postgres_roles.txt @@ -7,6 +7,8 @@ resources.postgres_roles.*.create_time *time.Time REMOTE resources.postgres_roles.*.id string INPUT resources.postgres_roles.*.identity_type postgres.RoleIdentityType ALL resources.postgres_roles.*.lifecycle resources.Lifecycle INPUT +resources.postgres_roles.*.lifecycle.bind *resources.Bind INPUT +resources.postgres_roles.*.lifecycle.bind.id string INPUT resources.postgres_roles.*.lifecycle.depends_on []resources.DependsOn INPUT resources.postgres_roles.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.postgres_roles.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/postgres_snapshot_schedules.txt b/acceptance/bundle/refschema/output/postgres_snapshot_schedules.txt index ae44e6072ca..12046615c49 100644 --- a/acceptance/bundle/refschema/output/postgres_snapshot_schedules.txt +++ b/acceptance/bundle/refschema/output/postgres_snapshot_schedules.txt @@ -1,6 +1,8 @@ resources.postgres_snapshot_schedules.*.branch string ALL resources.postgres_snapshot_schedules.*.id string INPUT resources.postgres_snapshot_schedules.*.lifecycle resources.Lifecycle INPUT +resources.postgres_snapshot_schedules.*.lifecycle.bind *resources.Bind INPUT +resources.postgres_snapshot_schedules.*.lifecycle.bind.id string INPUT resources.postgres_snapshot_schedules.*.lifecycle.depends_on []resources.DependsOn INPUT resources.postgres_snapshot_schedules.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.postgres_snapshot_schedules.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/postgres_synced_tables.txt b/acceptance/bundle/refschema/output/postgres_synced_tables.txt index 78be11c8847..0da83f70495 100644 --- a/acceptance/bundle/refschema/output/postgres_synced_tables.txt +++ b/acceptance/bundle/refschema/output/postgres_synced_tables.txt @@ -11,6 +11,8 @@ resources.postgres_synced_tables.*.extra_columns[*].compute string ALL resources.postgres_synced_tables.*.extra_columns[*].maintenance postgres.SyncedTableSyncedTableSpecExtraColumnMaintenance ALL resources.postgres_synced_tables.*.id string INPUT resources.postgres_synced_tables.*.lifecycle resources.Lifecycle INPUT +resources.postgres_synced_tables.*.lifecycle.bind *resources.Bind INPUT +resources.postgres_synced_tables.*.lifecycle.bind.id string INPUT resources.postgres_synced_tables.*.lifecycle.depends_on []resources.DependsOn INPUT resources.postgres_synced_tables.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.postgres_synced_tables.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/quality_monitors.txt b/acceptance/bundle/refschema/output/quality_monitors.txt index 5bd3f952fbd..e51d38e3eaa 100644 --- a/acceptance/bundle/refschema/output/quality_monitors.txt +++ b/acceptance/bundle/refschema/output/quality_monitors.txt @@ -24,6 +24,8 @@ resources.quality_monitors.*.inference_log.problem_type catalog.MonitorInference resources.quality_monitors.*.inference_log.timestamp_col string ALL resources.quality_monitors.*.latest_monitor_failure_msg string ALL resources.quality_monitors.*.lifecycle resources.Lifecycle INPUT +resources.quality_monitors.*.lifecycle.bind *resources.Bind INPUT +resources.quality_monitors.*.lifecycle.bind.id string INPUT resources.quality_monitors.*.lifecycle.depends_on []resources.DependsOn INPUT resources.quality_monitors.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.quality_monitors.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/registered_models.txt b/acceptance/bundle/refschema/output/registered_models.txt index 7c3d1db0860..efc40ad54bb 100644 --- a/acceptance/bundle/refschema/output/registered_models.txt +++ b/acceptance/bundle/refschema/output/registered_models.txt @@ -14,6 +14,8 @@ resources.registered_models.*.created_by string ALL resources.registered_models.*.full_name string ALL resources.registered_models.*.id string INPUT resources.registered_models.*.lifecycle resources.Lifecycle INPUT +resources.registered_models.*.lifecycle.bind *resources.Bind INPUT +resources.registered_models.*.lifecycle.bind.id string INPUT resources.registered_models.*.lifecycle.depends_on []resources.DependsOn INPUT resources.registered_models.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.registered_models.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/schemas.txt b/acceptance/bundle/refschema/output/schemas.txt index 694d59c6211..70983f86e00 100644 --- a/acceptance/bundle/refschema/output/schemas.txt +++ b/acceptance/bundle/refschema/output/schemas.txt @@ -13,6 +13,8 @@ resources.schemas.*.enable_predictive_optimization catalog.EnablePredictiveOptim resources.schemas.*.full_name string REMOTE resources.schemas.*.id string INPUT resources.schemas.*.lifecycle resources.Lifecycle INPUT +resources.schemas.*.lifecycle.bind *resources.Bind INPUT +resources.schemas.*.lifecycle.bind.id string INPUT resources.schemas.*.lifecycle.depends_on []resources.DependsOn INPUT resources.schemas.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.schemas.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/secret_scopes.txt b/acceptance/bundle/refschema/output/secret_scopes.txt index 832a476d47a..793e8591de9 100644 --- a/acceptance/bundle/refschema/output/secret_scopes.txt +++ b/acceptance/bundle/refschema/output/secret_scopes.txt @@ -8,6 +8,8 @@ resources.secret_scopes.*.keyvault_metadata *workspace.AzureKeyVaultSecretScopeM resources.secret_scopes.*.keyvault_metadata.dns_name string INPUT resources.secret_scopes.*.keyvault_metadata.resource_id string INPUT resources.secret_scopes.*.lifecycle resources.Lifecycle INPUT +resources.secret_scopes.*.lifecycle.bind *resources.Bind INPUT +resources.secret_scopes.*.lifecycle.bind.id string INPUT resources.secret_scopes.*.lifecycle.depends_on []resources.DependsOn INPUT resources.secret_scopes.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.secret_scopes.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/secrets.txt b/acceptance/bundle/refschema/output/secrets.txt index f0c4b2b213a..29f5e5f454f 100644 --- a/acceptance/bundle/refschema/output/secrets.txt +++ b/acceptance/bundle/refschema/output/secrets.txt @@ -8,6 +8,8 @@ resources.secrets.*.expire_time *time.Time ALL resources.secrets.*.full_name string ALL resources.secrets.*.id string INPUT resources.secrets.*.lifecycle resources.Lifecycle INPUT +resources.secrets.*.lifecycle.bind *resources.Bind INPUT +resources.secrets.*.lifecycle.bind.id string INPUT resources.secrets.*.lifecycle.depends_on []resources.DependsOn INPUT resources.secrets.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.secrets.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/sql_warehouses.txt b/acceptance/bundle/refschema/output/sql_warehouses.txt index a642c226ac9..f7c11cb8caf 100644 --- a/acceptance/bundle/refschema/output/sql_warehouses.txt +++ b/acceptance/bundle/refschema/output/sql_warehouses.txt @@ -23,6 +23,8 @@ resources.sql_warehouses.*.jdbc_url string REMOTE resources.sql_warehouses.*.lifecycle *dresources.StateLifecycle REMOTE STATE resources.sql_warehouses.*.lifecycle *resources.LifecycleWithStarted INPUT resources.sql_warehouses.*.lifecycle resources.Lifecycle INPUT +resources.sql_warehouses.*.lifecycle.bind *resources.Bind INPUT +resources.sql_warehouses.*.lifecycle.bind.id string INPUT resources.sql_warehouses.*.lifecycle.depends_on []resources.DependsOn INPUT resources.sql_warehouses.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.sql_warehouses.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/synced_database_tables.txt b/acceptance/bundle/refschema/output/synced_database_tables.txt index 7836f00bb8b..7cc0ca860a4 100644 --- a/acceptance/bundle/refschema/output/synced_database_tables.txt +++ b/acceptance/bundle/refschema/output/synced_database_tables.txt @@ -44,6 +44,8 @@ resources.synced_database_tables.*.effective_database_instance_name string ALL resources.synced_database_tables.*.effective_logical_database_name string ALL resources.synced_database_tables.*.id string INPUT resources.synced_database_tables.*.lifecycle resources.Lifecycle INPUT +resources.synced_database_tables.*.lifecycle.bind *resources.Bind INPUT +resources.synced_database_tables.*.lifecycle.bind.id string INPUT resources.synced_database_tables.*.lifecycle.depends_on []resources.DependsOn INPUT resources.synced_database_tables.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.synced_database_tables.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/vector_search_endpoints.txt b/acceptance/bundle/refschema/output/vector_search_endpoints.txt index 7fddb5b8e05..48947c4302f 100644 --- a/acceptance/bundle/refschema/output/vector_search_endpoints.txt +++ b/acceptance/bundle/refschema/output/vector_search_endpoints.txt @@ -15,6 +15,8 @@ resources.vector_search_endpoints.*.id string INPUT REMOTE resources.vector_search_endpoints.*.last_updated_timestamp int64 REMOTE resources.vector_search_endpoints.*.last_updated_user string REMOTE resources.vector_search_endpoints.*.lifecycle resources.Lifecycle INPUT +resources.vector_search_endpoints.*.lifecycle.bind *resources.Bind INPUT +resources.vector_search_endpoints.*.lifecycle.bind.id string INPUT resources.vector_search_endpoints.*.lifecycle.depends_on []resources.DependsOn INPUT resources.vector_search_endpoints.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.vector_search_endpoints.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/vector_search_indexes.txt b/acceptance/bundle/refschema/output/vector_search_indexes.txt index 6246bf1fdc7..24a5807f541 100644 --- a/acceptance/bundle/refschema/output/vector_search_indexes.txt +++ b/acceptance/bundle/refschema/output/vector_search_indexes.txt @@ -36,6 +36,8 @@ resources.vector_search_indexes.*.id string INPUT resources.vector_search_indexes.*.index_subtype vectorsearch.IndexSubtype ALL resources.vector_search_indexes.*.index_type vectorsearch.VectorIndexType ALL resources.vector_search_indexes.*.lifecycle resources.Lifecycle INPUT +resources.vector_search_indexes.*.lifecycle.bind *resources.Bind INPUT +resources.vector_search_indexes.*.lifecycle.bind.id string INPUT resources.vector_search_indexes.*.lifecycle.depends_on []resources.DependsOn INPUT resources.vector_search_indexes.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.vector_search_indexes.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/volumes.txt b/acceptance/bundle/refschema/output/volumes.txt index 1c573ed1931..c34d267fbd7 100644 --- a/acceptance/bundle/refschema/output/volumes.txt +++ b/acceptance/bundle/refschema/output/volumes.txt @@ -11,6 +11,8 @@ resources.volumes.*.encryption_details.sse_encryption_details.aws_kms_key_arn st resources.volumes.*.full_name string REMOTE resources.volumes.*.id string INPUT resources.volumes.*.lifecycle resources.Lifecycle INPUT +resources.volumes.*.lifecycle.bind *resources.Bind INPUT +resources.volumes.*.lifecycle.bind.id string INPUT resources.volumes.*.lifecycle.depends_on []resources.DependsOn INPUT resources.volumes.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.volumes.*.lifecycle.depends_on[*].resource string INPUT From fb9b76d9faa3b3ab919485a3323d8cbbbd4fbcff Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 17:04:48 +0000 Subject: [PATCH 16/20] Avoid leading-slash arguments in the in-state recreate guard test Git Bash on Windows rewrites a leading-slash argument as a Windows path ('C:/Program Files/Git/old/storage/path'), so update_file.py did not find the value and the test failed on every Windows cloud runner. Match the path without its leading slash instead. Co-authored-by: Isaac --- .../bundle/deploy/bind/recreate-blocked-in-state/script | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/script b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/script index 517c9ed3949..e770f80d3cf 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/script +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/script @@ -13,9 +13,10 @@ trace $CLI bundle deploy --auto-approve # Change storage (immutable) while the bind block is still defined. The bind # block must keep guarding against recreate even after the resource is in state. -update_file.py databricks.yml '/old/storage/path' '/new/storage/path' +# No leading slash in the arguments: Git Bash on Windows would rewrite it as a path. +update_file.py databricks.yml 'old/storage/path' 'new/storage/path' trace musterr $CLI bundle plan # Cleanup -update_file.py databricks.yml '/new/storage/path' '/old/storage/path' +update_file.py databricks.yml 'new/storage/path' 'old/storage/path' trace $CLI bundle destroy --auto-approve From 0c0daf35332dd11148d640dee686f3459f38fa90 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Wed, 4 Nov 2026 15:21:18 +0000 Subject: [PATCH 17/20] Regenerate outputs for resources added on main data_quality_monitors and materialization_sets pick up lifecycle.bind in the reference schema, the required-field validation and the PyDABs models. Co-authored-by: Isaac --- .../output/data_quality_monitors.txt | 2 + .../refschema/output/materialization_sets.txt | 2 + .../validation/generated/required_fields.go | 2 + .../bundles/data_quality_monitors/__init__.py | 8 ++++ .../data_quality_monitors/_models/bind.py | 40 +++++++++++++++++++ .../_models/lifecycle.py | 14 +++++++ .../bundles/materialization_sets/__init__.py | 8 ++++ .../materialization_sets/_models/bind.py | 40 +++++++++++++++++++ .../materialization_sets/_models/lifecycle.py | 14 +++++++ 9 files changed, 130 insertions(+) create mode 100644 python/databricks/bundles/data_quality_monitors/_models/bind.py create mode 100644 python/databricks/bundles/materialization_sets/_models/bind.py diff --git a/acceptance/bundle/refschema/output/data_quality_monitors.txt b/acceptance/bundle/refschema/output/data_quality_monitors.txt index c8868667fae..2faf3d71439 100644 --- a/acceptance/bundle/refschema/output/data_quality_monitors.txt +++ b/acceptance/bundle/refschema/output/data_quality_monitors.txt @@ -74,6 +74,8 @@ resources.data_quality_monitors.*.data_profiling_config.time_series.timestamp_co resources.data_quality_monitors.*.data_profiling_config.warehouse_id string ALL resources.data_quality_monitors.*.id string INPUT resources.data_quality_monitors.*.lifecycle resources.Lifecycle INPUT +resources.data_quality_monitors.*.lifecycle.bind *resources.Bind INPUT +resources.data_quality_monitors.*.lifecycle.bind.id string INPUT resources.data_quality_monitors.*.lifecycle.depends_on []resources.DependsOn INPUT resources.data_quality_monitors.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.data_quality_monitors.*.lifecycle.depends_on[*].resource string INPUT diff --git a/acceptance/bundle/refschema/output/materialization_sets.txt b/acceptance/bundle/refschema/output/materialization_sets.txt index 14b53dcb382..cec5d4731f8 100644 --- a/acceptance/bundle/refschema/output/materialization_sets.txt +++ b/acceptance/bundle/refschema/output/materialization_sets.txt @@ -1,5 +1,7 @@ resources.materialization_sets.*.id string INPUT resources.materialization_sets.*.lifecycle resources.Lifecycle INPUT +resources.materialization_sets.*.lifecycle.bind *resources.Bind INPUT +resources.materialization_sets.*.lifecycle.bind.id string INPUT resources.materialization_sets.*.lifecycle.depends_on []resources.DependsOn INPUT resources.materialization_sets.*.lifecycle.depends_on[*] resources.DependsOn INPUT resources.materialization_sets.*.lifecycle.depends_on[*].resource string INPUT diff --git a/bundle/internal/validation/generated/required_fields.go b/bundle/internal/validation/generated/required_fields.go index 3170efa9174..1457b1879e6 100644 --- a/bundle/internal/validation/generated/required_fields.go +++ b/bundle/internal/validation/generated/required_fields.go @@ -82,6 +82,7 @@ var RequiredFields = map[string][]string{ "resources.data_quality_monitors.*.data_profiling_config.inference_log": {"granularities", "model_id_column", "prediction_column", "problem_type", "timestamp_column"}, "resources.data_quality_monitors.*.data_profiling_config.schedule": {"quartz_cron_expression", "timezone_id"}, "resources.data_quality_monitors.*.data_profiling_config.time_series": {"granularities", "timestamp_column"}, + "resources.data_quality_monitors.*.lifecycle.bind": {"id"}, "resources.data_quality_monitors.*.lifecycle.depends_on[*]": {"resource"}, "resources.database_catalogs.*": {"database_instance_name", "database_name", "name"}, @@ -314,6 +315,7 @@ var RequiredFields = map[string][]string{ "resources.jobs.*.webhook_notifications.on_streaming_backlog_exceeded[*]": {"id"}, "resources.jobs.*.webhook_notifications.on_success[*]": {"id"}, + "resources.materialization_sets.*.lifecycle.bind": {"id"}, "resources.materialization_sets.*.lifecycle.depends_on[*]": {"resource"}, "resources.materialization_sets.*.materialized_features[*]": {"feature_name"}, "resources.materialization_sets.*.materialized_features[*].offline_store_config": {"catalog_name", "schema_name", "table_name_prefix"}, diff --git a/python/databricks/bundles/data_quality_monitors/__init__.py b/python/databricks/bundles/data_quality_monitors/__init__.py index 47c0aa88dc0..16125cb939a 100644 --- a/python/databricks/bundles/data_quality_monitors/__init__.py +++ b/python/databricks/bundles/data_quality_monitors/__init__.py @@ -6,6 +6,9 @@ "AnomalyDetectionConfig", "AnomalyDetectionConfigDict", "AnomalyDetectionConfigParam", + "Bind", + "BindDict", + "BindParam", "CronSchedule", "CronScheduleDict", "CronScheduleParam", @@ -55,6 +58,11 @@ AnomalyDetectionConfigDict, AnomalyDetectionConfigParam, ) +from databricks.bundles.data_quality_monitors._models.bind import ( + Bind, + BindDict, + BindParam, +) from databricks.bundles.data_quality_monitors._models.cron_schedule import ( CronSchedule, CronScheduleDict, diff --git a/python/databricks/bundles/data_quality_monitors/_models/bind.py b/python/databricks/bundles/data_quality_monitors/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/data_quality_monitors/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/data_quality_monitors/_models/lifecycle.py b/python/databricks/bundles/data_quality_monitors/_models/lifecycle.py index 2643a1c4e95..8d1174d9687 100644 --- a/python/databricks/bundles/data_quality_monitors/_models/lifecycle.py +++ b/python/databricks/bundles/data_quality_monitors/_models/lifecycle.py @@ -6,6 +6,10 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.data_quality_monitors._models.bind import ( + Bind, + BindParam, +) from databricks.bundles.data_quality_monitors._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +23,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +49,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. diff --git a/python/databricks/bundles/materialization_sets/__init__.py b/python/databricks/bundles/materialization_sets/__init__.py index a7e26eaaf07..41b7e9567e2 100644 --- a/python/databricks/bundles/materialization_sets/__init__.py +++ b/python/databricks/bundles/materialization_sets/__init__.py @@ -1,6 +1,9 @@ # Code generated by pydabs-codegen. DO NOT EDIT. __all__ = [ + "Bind", + "BindDict", + "BindParam", "CronSchedule", "CronScheduleDict", "CronScheduleMode", @@ -46,6 +49,11 @@ ] +from databricks.bundles.materialization_sets._models.bind import ( + Bind, + BindDict, + BindParam, +) from databricks.bundles.materialization_sets._models.cron_schedule import ( CronSchedule, CronScheduleDict, diff --git a/python/databricks/bundles/materialization_sets/_models/bind.py b/python/databricks/bundles/materialization_sets/_models/bind.py new file mode 100644 index 00000000000..35f09d15949 --- /dev/null +++ b/python/databricks/bundles/materialization_sets/_models/bind.py @@ -0,0 +1,40 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Bind: + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + @classmethod + def from_dict(cls, value: "BindDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "BindDict": + return _transform_to_json_value(self) # type:ignore + + +class BindDict(TypedDict, total=False): + """""" + + id: VariableOr[str] + """ + The ID of the existing workspace resource, as used by its API (for example a job ID, or the full name of a schema or volume). Set it per target through a target override of the resource; variable references are resolved. + """ + + +BindParam = BindDict | Bind diff --git a/python/databricks/bundles/materialization_sets/_models/lifecycle.py b/python/databricks/bundles/materialization_sets/_models/lifecycle.py index e2721031364..d47da9a7510 100644 --- a/python/databricks/bundles/materialization_sets/_models/lifecycle.py +++ b/python/databricks/bundles/materialization_sets/_models/lifecycle.py @@ -6,6 +6,10 @@ from databricks.bundles.core._transform import _transform from databricks.bundles.core._transform_to_json import _transform_to_json_value from databricks.bundles.core._variable import VariableOrList, VariableOrOptional +from databricks.bundles.materialization_sets._models.bind import ( + Bind, + BindParam, +) from databricks.bundles.materialization_sets._models.depends_on import ( DependsOn, DependsOnParam, @@ -19,6 +23,11 @@ class Lifecycle: """""" + bind: VariableOrOptional[Bind] = None + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOn] = field(default_factory=list) """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -40,6 +49,11 @@ def as_dict(self) -> "LifecycleDict": class LifecycleDict(TypedDict, total=False): """""" + bind: VariableOrOptional[BindParam] + """ + Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + """ + depends_on: VariableOrList[DependsOnParam] """ Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. From a4a8e43378fd6ae7d44f3359bb8d494d3e2d1765 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Wed, 4 Nov 2026 22:35:44 +0000 Subject: [PATCH 18/20] Link the new pull request from the bind changelog fragment Co-authored-by: Isaac --- .nextchanges/bundles/4630.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nextchanges/bundles/4630.md b/.nextchanges/bundles/4630.md index 73d60dfceb6..60920ad6d77 100644 --- a/.nextchanges/bundles/4630.md +++ b/.nextchanges/bundles/4630.md @@ -1 +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)) +* 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), [#7366](https://github.com/databricks/cli/pull/7366)) From 6edb41a6ea8e606c5293599d6cd376b8765b179d Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Thu, 5 Nov 2026 11:16:12 +0000 Subject: [PATCH 19/20] Fix declarative bind state handling and acceptance coverage --- .nextchanges/bundles/4630.md | 1 - .nextchanges/bundles/7366.md | 1 + .../bundle/deploy/bind/basic/databricks.yml | 15 +- acceptance/bundle/deploy/bind/basic/hello.py | 1 - .../bundle/deploy/bind/basic/output.txt | 45 +---- acceptance/bundle/deploy/bind/basic/script | 12 +- .../bind/bind-and-resize/databricks.yml.tmpl | 4 + .../deploy/bind/bind-and-resize/output.txt | 4 + .../bundle/deploy/bind/bind-and-resize/script | 35 +++- .../bind/block-removed/databricks_unbound.yml | 2 +- .../deploy/bind/block-removed/output.txt | 14 +- .../bundle/deploy/bind/block-removed/script | 8 +- .../bundle/deploy/bind/pure-bind/output.txt | 18 ++ .../bundle/deploy/bind/pure-bind/script | 4 + .../bind/recreate-blocked-in-state/output.txt | 24 +-- .../deploy/bind/recreate-blocked/output.txt | 24 +-- .../deploy/bind/variable-id/databricks.yml | 1 - .../bundle/deploy/bind/variable-id/output.txt | 6 +- .../bundle/deploy/bind/variable-id/script | 8 +- bundle/deployplan/action.go | 6 +- bundle/deployplan/action_test.go | 25 +++ bundle/direct/apply.go | 2 +- bundle/direct/bind_test.go | 163 ++++++++++++++++++ bundle/direct/bundle_plan.go | 8 +- bundle/direct/bundle_plan_bind.go | 49 +----- bundle/phases/bind.go | 10 ++ 26 files changed, 324 insertions(+), 166 deletions(-) delete mode 100644 .nextchanges/bundles/4630.md create mode 100644 .nextchanges/bundles/7366.md delete mode 100644 acceptance/bundle/deploy/bind/basic/hello.py create mode 100644 bundle/deployplan/action_test.go create mode 100644 bundle/direct/bind_test.go diff --git a/.nextchanges/bundles/4630.md b/.nextchanges/bundles/4630.md deleted file mode 100644 index 60920ad6d77..00000000000 --- a/.nextchanges/bundles/4630.md +++ /dev/null @@ -1 +0,0 @@ -* 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), [#7366](https://github.com/databricks/cli/pull/7366)) diff --git a/.nextchanges/bundles/7366.md b/.nextchanges/bundles/7366.md new file mode 100644 index 00000000000..8098357bd09 --- /dev/null +++ b/.nextchanges/bundles/7366.md @@ -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. ([#7366](https://github.com/databricks/cli/pull/7366)) diff --git a/acceptance/bundle/deploy/bind/basic/databricks.yml b/acceptance/bundle/deploy/bind/basic/databricks.yml index 5a33ba1ced8..4d627f32c87 100644 --- a/acceptance/bundle/deploy/bind/basic/databricks.yml +++ b/acceptance/bundle/deploy/bind/basic/databricks.yml @@ -2,18 +2,9 @@ bundle: name: test-bind-basic-UNIQUE_NAME resources: - jobs: + experiments: 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 + name: /Shared/test-bind-basic-UNIQUE_NAME lifecycle: bind: - id: "PLACEHOLDER_JOB_ID" + id: "PLACEHOLDER_EXPERIMENT_ID" diff --git a/acceptance/bundle/deploy/bind/basic/hello.py b/acceptance/bundle/deploy/bind/basic/hello.py deleted file mode 100644 index 11b15b1a458..00000000000 --- a/acceptance/bundle/deploy/bind/basic/hello.py +++ /dev/null @@ -1 +0,0 @@ -print("hello") diff --git a/acceptance/bundle/deploy/bind/basic/output.txt b/acceptance/bundle/deploy/bind/basic/output.txt index 4bbb8bff679..05e482d68f3 100644 --- a/acceptance/bundle/deploy/bind/basic/output.txt +++ b/acceptance/bundle/deploy/bind/basic/output.txt @@ -1,14 +1,14 @@ >>> [CLI] bundle plan -bind_and_update jobs.foo (id: [NEW_JOB_ID]) +bind experiments.foo (id: [NEW_EXPERIMENT_ID]) -Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged, 1 to bind +Plan: 0 to add, 0 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 +Bound experiments.foo +Files: 1 uploaded, 0 deleted +Resources: 0 created, 0 changed, 0 deleted, 0 unchanged, 1 bound >>> [CLI] bundle plan Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged @@ -20,37 +20,10 @@ Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged "lineage": "[UUID]", "serial": 1, "state": { - "resources.jobs.foo": { - "__id__": "[NEW_JOB_ID]", + "resources.experiments.foo": { + "__id__": "[NEW_EXPERIMENT_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" - } - ] + "name": "/Shared/test-bind-basic-[UNIQUE_NAME]" } } } @@ -58,7 +31,7 @@ Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged >>> [CLI] bundle destroy --auto-approve The following resources will be deleted: - delete resources.jobs.foo + delete resources.experiments.foo All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-basic-[UNIQUE_NAME]/default diff --git a/acceptance/bundle/deploy/bind/basic/script b/acceptance/bundle/deploy/bind/basic/script index 557be0c9b33..21a52688afc 100644 --- a/acceptance/bundle/deploy/bind/basic/script +++ b/acceptance/bundle/deploy/bind/basic/script @@ -2,12 +2,12 @@ # 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 +# Create an experiment whose configuration already matches the bundle. +NEW_EXPERIMENT_ID=$(MSYS_NO_PATHCONV=1 $CLI experiments create-experiment "/Shared/test-bind-basic-$UNIQUE_NAME" | jq -r .experiment_id) +add_repl "$NEW_EXPERIMENT_ID" NEW_EXPERIMENT_ID -# Update the databricks.yml with the actual job ID -update_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" +# Update the databricks.yml with the actual experiment ID +update_file.py databricks.yml 'PLACEHOLDER_EXPERIMENT_ID' "$NEW_EXPERIMENT_ID" # Run plan - should show import action trace $CLI bundle plan @@ -19,7 +19,7 @@ trace $CLI bundle deploy --auto-approve trace $CLI bundle plan # Verify state file contains the imported ID -trace print_state.py | contains.py "$NEW_JOB_ID" +trace print_state.py | contains.py "$NEW_EXPERIMENT_ID" # Cleanup: destroy is allowed even with an active bind block trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl b/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl index 6332e9f172d..24d298d5272 100644 --- a/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl +++ b/acceptance/bundle/deploy/bind/bind-and-resize/databricks.yml.tmpl @@ -4,6 +4,10 @@ bundle: workspace: root_path: ~/.bundle/$UNIQUE_NAME +sync: + exclude: + - LOG.* + resources: clusters: test_cluster: diff --git a/acceptance/bundle/deploy/bind/bind-and-resize/output.txt b/acceptance/bundle/deploy/bind/bind-and-resize/output.txt index 5e7ecc9f876..9fb0ff28f18 100644 --- a/acceptance/bundle/deploy/bind/bind-and-resize/output.txt +++ b/acceptance/bundle/deploy/bind/bind-and-resize/output.txt @@ -1,4 +1,8 @@ +=== Existing cluster has 2 workers; bundle configuration requests 3 +>>> [CLI] clusters get [CLUSTER_ID] +json.num_workers = 2; + >>> [CLI] bundle plan bind_and_update clusters.test_cluster (id: [CLUSTER_ID]) diff --git a/acceptance/bundle/deploy/bind/bind-and-resize/script b/acceptance/bundle/deploy/bind/bind-and-resize/script index 271eaf9d447..5779afb0c4b 100644 --- a/acceptance/bundle/deploy/bind/bind-and-resize/script +++ b/acceptance/bundle/deploy/bind/bind-and-resize/script @@ -1,14 +1,33 @@ 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 +# The shared AWS pool uses Spot in a fixed AZ and can exhaust capacity before +# this test reaches bind planning. Use on-demand capacity with automatic AZ selection. +COMPUTE=$(jq -n --arg pool "$TEST_INSTANCE_POOL_ID" '{instance_pool_id: $pool}') +if [[ "$CLOUD_ENV_BASE" == aws ]]; then + COMPUTE=$(jq -n --arg node "$NODE_TYPE_ID" '{node_type_id: $node, aws_attributes: {availability: "ON_DEMAND", zone_id: "auto"}}') + update_file.py databricks.yml "instance_pool_id: $TEST_INSTANCE_POOL_ID" "node_type_id: $NODE_TYPE_ID + aws_attributes: + availability: ON_DEMAND + zone_id: auto" +fi +jq -n --arg name "test-cluster-$UNIQUE_NAME" --arg spark "$DEFAULT_SPARK_VERSION" --argjson compute "$COMPUTE" \ + '$compute + {cluster_name: $name, spark_version: $spark, num_workers: 2, autotermination_minutes: 60}' > LOG.create.json +CLUSTER_ID=$($CLI clusters create --no-wait --json @LOG.create.json | jq -r .cluster_id) +add_repl "$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 +cleanup() { + if [[ -n "$CLUSTER_ID" ]]; then + $CLI clusters permanent-delete "$CLUSTER_ID" &> LOG.cluster-cleanup + fi + rm -f "$OUT_REQUESTS" +} +trap cleanup EXIT + +RETRY_MAX_ATTEMPTS=120 RETRY_INTERVAL_MS=10000 retry --until '"state": "RUNNING"' $CLI clusters get "$CLUSTER_ID" > LOG.wait + +title "Existing cluster has 2 workers; bundle configuration requests 3" +trace $CLI clusters get "$CLUSTER_ID" | gron.py | grep '^json.num_workers = ' # Plan: config differs only in num_workers, so the bound running cluster is # planned as bind_and_update backed by a resize. @@ -23,4 +42,4 @@ trace $CLI clusters get "$CLUSTER_ID" | jq '{cluster_name,num_workers}' # Cleanup trace $CLI bundle destroy --auto-approve -rm -f out.requests.txt +CLUSTER_ID="" diff --git a/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml b/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml index 1632bfce959..99670d4f8c3 100644 --- a/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml +++ b/acceptance/bundle/deploy/bind/block-removed/databricks_unbound.yml @@ -5,7 +5,7 @@ resources: pipelines: foo: name: test-pipeline - storage: /new/storage/path + storage: /old/storage/path libraries: - notebook: path: ./nb.sql diff --git a/acceptance/bundle/deploy/bind/block-removed/output.txt b/acceptance/bundle/deploy/bind/block-removed/output.txt index 33cba18f69d..c51a96dd573 100644 --- a/acceptance/bundle/deploy/bind/block-removed/output.txt +++ b/acceptance/bundle/deploy/bind/block-removed/output.txt @@ -5,7 +5,19 @@ 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 +=== Removing only the bind block is a no-op +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-block-removed-[UNIQUE_NAME]/default/files... +Files: 1 uploaded, 0 deleted +Resources: 0 created, 0 changed, 0 deleted, 1 unchanged + +>>> print_state.py +json.state.resources.pipelines.foo.__id__ = "[NEW_PIPELINE_ID]"; + +=== Without the bind block, a storage change plans a recreate >>> [CLI] bundle plan recreate pipelines.foo diff --git a/acceptance/bundle/deploy/bind/block-removed/script b/acceptance/bundle/deploy/bind/block-removed/script index 4b75a1748be..f70af112b3d 100644 --- a/acceptance/bundle/deploy/bind/block-removed/script +++ b/acceptance/bundle/deploy/bind/block-removed/script @@ -8,9 +8,15 @@ 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" +title "Removing only the bind block is a no-op" cp databricks_unbound.yml databricks.yml trace $CLI bundle plan +trace $CLI bundle deploy --auto-approve +trace print_state.py | gron.py | grep __id__ | contains.py "$NEW_PIPELINE_ID" + +title "Without the bind block, a storage change plans a recreate" +update_file.py databricks.yml "/old/storage/path" "/new/storage/path" +trace $CLI bundle plan # Cleanup trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deploy/bind/pure-bind/output.txt b/acceptance/bundle/deploy/bind/pure-bind/output.txt index 6ad11bb2f42..7a98a5bbe13 100644 --- a/acceptance/bundle/deploy/bind/pure-bind/output.txt +++ b/acceptance/bundle/deploy/bind/pure-bind/output.txt @@ -13,6 +13,21 @@ Warning: resources.jobs.foo.lifecycle.bind re-binds the resource on the next dep in databricks.yml:22:11 +=== Imperative bind warns that the declarative bind block is redundant +>>> [CLI] bundle deployment bind foo [NEW_JOB_ID] --auto-approve +Warning: resources.jobs.foo.lifecycle.bind already declares the resource ID; use 'bundle deploy' to bind it declaratively + at resources.jobs.foo.lifecycle.bind + in databricks.yml:22:11 + +Successfully bound job with an id '[NEW_JOB_ID]' +Run 'bundle deploy' to deploy changes to your workspace + +>>> [CLI] bundle deployment unbind foo +Warning: resources.jobs.foo.lifecycle.bind re-binds the resource on the next deploy; remove it to keep the resource unbound + at resources.jobs.foo.lifecycle.bind + in databricks.yml:22:11 + + === The job now matches the config, so this is a pure bind >>> [CLI] bundle plan bind jobs.foo (id: [NEW_JOB_ID]) @@ -29,6 +44,9 @@ json.plan.resources.jobs.foo.action = "bind"; {"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} {"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} {"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} >>> [CLI] bundle deploy --auto-approve Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-pure-[UNIQUE_NAME]/default/files... diff --git a/acceptance/bundle/deploy/bind/pure-bind/script b/acceptance/bundle/deploy/bind/pure-bind/script index 45a1f4a056b..b2b56968098 100644 --- a/acceptance/bundle/deploy/bind/pure-bind/script +++ b/acceptance/bundle/deploy/bind/pure-bind/script @@ -11,6 +11,10 @@ trace $CLI bundle deploy --auto-approve title "Unbind the job. The bind block is still in config, so the next deploy re-binds it" trace $CLI bundle deployment unbind foo +title "Imperative bind warns that the declarative bind block is redundant" +trace $CLI bundle deployment bind foo "$NEW_JOB_ID" --auto-approve +trace $CLI bundle deployment unbind foo + title "The job now matches the config, so this is a pure bind" trace $CLI bundle plan trace $CLI bundle plan -o json | gron.py | grep -E 'jobs\.foo\.(action|bind_id)' diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt index 69a32ffc21c..34f11e65065 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt @@ -8,30 +8,10 @@ Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound >>> musterr [CLI] bundle plan Error: cannot plan resources.pipelines.foo: lifecycle.bind prevents recreating the bound resource -This would destroy and recreate the bound workspace resource, changing its ID. - -The following fields cannot be modified because they require resource recreation: +Fields requiring resource recreation: - storage (immutable) -To resolve this issue, you have two options: - -1. Revert the problematic fields in your configuration to keep the bound resource as it is: - - resources: - pipelines: - foo: - # Remove or comment out these fields: - # storage: ... - -2. Remove lifecycle.bind if you want to allow the resource to be recreated/updated: - - resources: - pipelines: - foo: - lifecycle: - # Remove the bind block: - # bind: - # id: +Match these fields to the existing resource to preserve its ID; remove lifecycle.bind to allow the ID to change Error: planning failed diff --git a/acceptance/bundle/deploy/bind/recreate-blocked/output.txt b/acceptance/bundle/deploy/bind/recreate-blocked/output.txt index 51588345908..31e087ae7a0 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked/output.txt +++ b/acceptance/bundle/deploy/bind/recreate-blocked/output.txt @@ -2,30 +2,10 @@ >>> musterr [CLI] bundle plan Error: cannot bind resources.pipelines.foo: cannot recreate resource with lifecycle.bind -This would destroy and recreate the existing workspace resource, changing its ID. - -The following fields cannot be modified because they require resource recreation: +Fields requiring resource recreation: - storage (immutable) -To resolve this issue, you have two options: - -1. Remove the problematic fields from your configuration to make this a bind-only operation: - - resources: - pipelines: - foo: - # Remove or comment out these fields: - # storage: ... - -2. Remove lifecycle.bind if you want to allow the resource to be recreated/updated: - - resources: - pipelines: - foo: - lifecycle: - # Remove the bind block: - # bind: - # id: +Match these fields to the existing resource to preserve its ID; remove lifecycle.bind to create a new resource instead of adopting this one Error: planning failed diff --git a/acceptance/bundle/deploy/bind/variable-id/databricks.yml b/acceptance/bundle/deploy/bind/variable-id/databricks.yml index f266dec8262..69b6ad12019 100644 --- a/acceptance/bundle/deploy/bind/variable-id/databricks.yml +++ b/acceptance/bundle/deploy/bind/variable-id/databricks.yml @@ -4,7 +4,6 @@ bundle: variables: job_id: description: ID of the existing job to bind - default: "PLACEHOLDER_JOB_ID" resources: jobs: diff --git a/acceptance/bundle/deploy/bind/variable-id/output.txt b/acceptance/bundle/deploy/bind/variable-id/output.txt index 99a13d92977..9aec503d1e0 100644 --- a/acceptance/bundle/deploy/bind/variable-id/output.txt +++ b/acceptance/bundle/deploy/bind/variable-id/output.txt @@ -1,10 +1,10 @@ ->>> [CLI] bundle plan +>>> [CLI] bundle plan --var job_id=[NEW_JOB_ID] 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 +>>> [CLI] bundle deploy --auto-approve --var job_id=[NEW_JOB_ID] Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-variable-id-[UNIQUE_NAME]/default/files... Bound and updated jobs.foo Files: 1 uploaded, 0 deleted @@ -13,7 +13,7 @@ Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound >>> print_state.py json.state.resources.jobs.foo.__id__ = "[NEW_JOB_ID]"; ->>> [CLI] bundle destroy --auto-approve +>>> [CLI] bundle destroy --auto-approve --var job_id=[NEW_JOB_ID] The following resources will be deleted: delete resources.jobs.foo diff --git a/acceptance/bundle/deploy/bind/variable-id/script b/acceptance/bundle/deploy/bind/variable-id/script index 0e9a511a8e2..21a2b7fd0ee 100644 --- a/acceptance/bundle/deploy/bind/variable-id/script +++ b/acceptance/bundle/deploy/bind/variable-id/script @@ -3,11 +3,9 @@ update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME" # Create a job in the workspace; its ID reaches the bind block through a variable NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" - -trace $CLI bundle plan -trace $CLI bundle deploy --auto-approve +trace $CLI bundle plan --var "job_id=$NEW_JOB_ID" +trace $CLI bundle deploy --auto-approve --var "job_id=$NEW_JOB_ID" trace print_state.py | gron.py | grep __id__ # Cleanup -trace $CLI bundle destroy --auto-approve +trace $CLI bundle destroy --auto-approve --var "job_id=$NEW_JOB_ID" diff --git a/bundle/deployplan/action.go b/bundle/deployplan/action.go index e85a5b63df6..e8f7149aa1a 100644 --- a/bundle/deployplan/action.go +++ b/bundle/deployplan/action.go @@ -40,7 +40,7 @@ func (a Action) IsChildResource() bool { type ActionType string // Actions are ordered in increasing severity. -// If case of several options, action with highest severity wins. +// In case of several options, action with highest severity wins. // Note, Create/Delete are handled explicitly and never compared. const ( Undefined ActionType = "" @@ -68,6 +68,8 @@ var actionOrder = map[ActionType]int{ Delete: 9, } +// KeepsID reports whether the action preserves an ID already recorded in deployment state. +// Bind actions have a remote ID but no existing state entry. func (a ActionType) KeepsID() bool { switch a { case Create, UpdateWithID, Recreate, Delete, Bind, BindAndUpdate: @@ -88,7 +90,7 @@ func (a ActionType) StringShort() string { return items[0] } -// AppliedLine renders the user-facing line reporting that action has been applied +// AppliedLine renders the user-facing line reporting that a non-Skip action has been applied // to resourceKey, e.g. "Created jobs.foo". The past-tense verb is the short action // name plus "d" (create->Created, delete->Deleted, ...), capitalized to match the // sentence case of other output. "bundle plan" keeps the lower-case present tense, diff --git a/bundle/deployplan/action_test.go b/bundle/deployplan/action_test.go new file mode 100644 index 00000000000..2ae3828fa5d --- /dev/null +++ b/bundle/deployplan/action_test.go @@ -0,0 +1,25 @@ +package deployplan_test + +import ( + "testing" + + "github.com/databricks/cli/bundle/deployplan" + "github.com/stretchr/testify/assert" +) + +func TestAppliedLine(t *testing.T) { + for action, expected := range map[deployplan.ActionType]string{ + deployplan.Create: "Created jobs.foo", + deployplan.Update: "Updated jobs.foo", + deployplan.UpdateWithID: "Updated jobs.foo", + deployplan.Resize: "Resized jobs.foo", + deployplan.Recreate: "Recreated jobs.foo", + deployplan.Delete: "Deleted jobs.foo", + deployplan.Bind: "Bound jobs.foo", + deployplan.BindAndUpdate: "Bound and updated jobs.foo", + } { + t.Run(string(action), func(t *testing.T) { + assert.Equal(t, expected, deployplan.AppliedLine("resources.jobs.foo", action)) + }) + } +} diff --git a/bundle/direct/apply.go b/bundle/direct/apply.go index 9e95c4cebc0..ab90e085278 100644 --- a/bundle/direct/apply.go +++ b/bundle/direct/apply.go @@ -76,7 +76,7 @@ func (d *DeploymentUnit) Bind(ctx context.Context, db *dstate.DeploymentState, i return err } - err = db.SaveState(ctx, d.ResourceKey, id, newState, d.DependsOn) + err = d.saveState(ctx, db, id, newState, d.DependsOn) if err != nil { return fmt.Errorf("saving state id=%s: %w", id, err) } diff --git a/bundle/direct/bind_test.go b/bundle/direct/bind_test.go new file mode 100644 index 00000000000..982fc883b71 --- /dev/null +++ b/bundle/direct/bind_test.go @@ -0,0 +1,163 @@ +package direct_test + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/bundle/deployplan" + "github.com/databricks/cli/bundle/direct" + "github.com/databricks/cli/bundle/direct/dresources" + "github.com/databricks/cli/bundle/direct/dstate" + "github.com/databricks/cli/libs/cmdctx" + "github.com/databricks/cli/libs/logdiag" + "github.com/databricks/cli/libs/testserver" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/service/bundledeployments" + "github.com/databricks/databricks-sdk-go/service/catalog" + "github.com/databricks/databricks-sdk-go/service/postgres" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func bindTestClient(t *testing.T) (*testserver.Server, *databricks.WorkspaceClient) { + t.Helper() + server := testserver.New(t) + testserver.AddDefaultHandlers(server) + client, err := databricks.NewWorkspaceClient(&databricks.Config{Host: server.URL, Token: "testtoken"}) + require.NoError(t, err) + return server, client +} + +func TestPureBindSanitizesPersistedState(t *testing.T) { + const secret = "synthetic-sensitive-value" + content := strings.Repeat("dashboard content ", 20) + for _, mode := range []dstate.Mode{ + dstate.WritingFile, + dstate.WritingDeploymentHistoryShadow, + dstate.WritingDeploymentHistoryOnly, + } { + for _, resourceType := range []string{"secrets", "dashboards"} { + t.Run(fmt.Sprintf("%d/%s", mode, resourceType), func(t *testing.T) { + server, client := bindTestClient(t) + recorded := make(chan string, 1) + server.Handle("PATCH", "/api/2.0/bundle/deployments/123/versions/1/operations/{key}", func(req testserver.Request) any { + var operation bundledeployments.Operation + assert.NoError(t, json.Unmarshal(req.Body, &operation)) + recorded <- operation.State + return bundledeployments.Operation{SequenceId: 1} + }) + ctx := cmdctx.SetWorkspaceClient(t.Context(), client) + ctx = logdiag.InitContext(ctx) + statePath := filepath.Join(t.TempDir(), "resources.json") + var stateDB dstate.DeploymentState + require.NoError(t, stateDB.Open(ctx, statePath, mode, dstate.WithRecovery(true), dstate.OpenDmsArgs{})) + t.Cleanup(func() { + _, err := stateDB.Finalize(ctx) + assert.NoError(t, err) + }) + if mode != dstate.WritingFile { + stateDB.InitializeOperationBuffer(ctx, "123", 1, 1) + } + adapter, err := dresources.NewAdapter(dresources.SupportedResources[resourceType], resourceType, client) + require.NoError(t, err) + unit := direct.DeploymentUnit{ + ResourceKey: "resources." + resourceType + ".test", + Adapter: adapter, + } + var newState any = &catalog.Secret{Name: "test", Value: secret, EffectiveValue: secret} + rawContent := secret + if resourceType == "dashboards" { + newState = &dresources.DashboardState{ + SerializedDashboard: content, + } + rawContent = content + } + require.NoError(t, unit.Bind(ctx, &stateDB, "remote-id", newState, &deployplan.PlanEntry{})) + assert.Equal(t, "remote-id", stateDB.GetResourceID(unit.ResourceKey)) + if mode != dstate.WritingDeploymentHistoryOnly { + wal, err := os.ReadFile(statePath + ".wal") + require.NoError(t, err) + assert.NotContains(t, string(wal), rawContent) + if resourceType == "dashboards" { + assert.Contains(t, string(wal), fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(content)))) + } + } + _, err = stateDB.Finalize(ctx) + require.NoError(t, err) + persisted, err := os.ReadFile(statePath) + require.NoError(t, err) + assert.NotContains(t, string(persisted), rawContent) + if mode != dstate.WritingFile { + require.Len(t, recorded, 1) + history := <-recorded + assert.NotContains(t, history, rawContent) + assert.Contains(t, history, `"state":`) + if resourceType == "dashboards" { + assert.Contains(t, history, fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(content)))) + } + } + }) + } + } +} + +func TestBindResolvesStableParentOutputs(t *testing.T) { + for _, parentAction := range []deployplan.ActionType{deployplan.Bind, deployplan.BindAndUpdate} { + for _, managedChild := range []bool{false, true} { + for _, parent := range []string{"projects/p", "${resources.postgres_projects.project.name}"} { + t.Run(fmt.Sprintf("%s/managed=%t/%s", parentAction, managedChild, parent), func(t *testing.T) { + server, client := bindTestClient(t) + server.Handle("GET", "/api/2.0/postgres/projects/p", func(req testserver.Request) any { + return postgres.Project{ + Name: "projects/p", + ProjectId: "p", + Spec: &postgres.ProjectSpec{DisplayName: "existing project"}, + } + }) + server.Handle("GET", "/api/2.0/postgres/projects/p/branches/b", func(req testserver.Request) any { + return postgres.Branch{Name: "projects/p/branches/b", BranchId: "b", Parent: "projects/p"} + }) + source := `{"resources":{"postgres_projects":{"project":{"project_id":"p","display_name":"existing project","lifecycle":{"bind":{"id":"projects/p"}}}},"postgres_branches":{"branch":{"parent":"PARENT","branch_id":"b","lifecycle":{"bind":{"id":"projects/p/branches/b"}}}}}}` + if parentAction == deployplan.BindAndUpdate { + source = strings.ReplaceAll(source, "existing project", "updated project") + } + if managedChild { + source = strings.ReplaceAll(source, `,"lifecycle":{"bind":{"id":"projects/p/branches/b"}}`, "") + } + root, diagnostics := config.LoadFromBytes("databricks.yml", []byte(strings.ReplaceAll(source, "PARENT", parent))) + require.Empty(t, diagnostics) + ctx := logdiag.InitContext(t.Context()) + logdiag.SetCollect(ctx, true) + var deployment direct.DeploymentBundle + statePath := filepath.Join(t.TempDir(), "resources.json") + if managedChild { + require.NoError(t, deployment.StateDB.Open(ctx, statePath, dstate.WritingFile, dstate.WithRecovery(true), dstate.OpenDmsArgs{})) + require.NoError(t, deployment.StateDB.SaveState(ctx, "resources.postgres_branches.branch", "projects/p/branches/b", &resources.PostgresBranchConfig{Parent: "projects/p", BranchId: "b"}, nil)) + _, err := deployment.StateDB.Finalize(ctx) + require.NoError(t, err) + } + require.NoError(t, deployment.StateDB.Open(ctx, statePath, dstate.ReadingFile, dstate.WithRecovery(true), dstate.OpenDmsArgs{})) + t.Cleanup(func() { + _, err := deployment.StateDB.Finalize(ctx) + assert.NoError(t, err) + }) + plan, err := deployment.CalculatePlan(ctx, client, root) + require.NoError(t, err, "%v", logdiag.FlushCollected(ctx)) + assert.Equal(t, parentAction, plan.Plan["resources.postgres_projects.project"].Action) + expected := deployplan.Bind + if managedChild { + expected = deployplan.Skip + } + assert.Equal(t, expected, plan.Plan["resources.postgres_branches.branch"].Action) + }) + } + } + } +} diff --git a/bundle/direct/bundle_plan.go b/bundle/direct/bundle_plan.go index 2a1321c2fab..271086fe36e 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -1076,13 +1076,15 @@ func (b *DeploymentBundle) LookupReferencePreDeploy(ctx context.Context, path *s } // A field is safe to read from the remote cache when the target either has no - // changes (Skip) or keeps its ID and the field cannot have changed: an + // changes (Skip or Bind) or retains its remote identity (including BindAndUpdate) + // and the field cannot have changed: an // immutable field (FieldTriggersRecreate) or a backend-assigned stable output // like an AIP name derived from the ID (FieldIsStableOutput). The latter lets // ${resources.X.name} resolve during an in-place update instead of delaying, // which would otherwise recreate dependents that reference it as their parent. - canReadRemoteCache := targetAction == deployplan.Skip || - (targetAction.KeepsID() && (adapter.FieldTriggersRecreate(fieldPath) || adapter.FieldIsStableOutput(fieldPath))) + canReadRemoteCache := targetAction == deployplan.Skip || targetAction == deployplan.Bind || + ((targetAction.KeepsID() || targetAction.IsBind()) && + (adapter.FieldTriggersRecreate(fieldPath) || adapter.FieldIsStableOutput(fieldPath))) if configValidErr != nil && remoteValidErr == nil { // The field is only present in remote state schema. diff --git a/bundle/direct/bundle_plan_bind.go b/bundle/direct/bundle_plan_bind.go index 11f26c5f1b4..9b99b3184b9 100644 --- a/bundle/direct/bundle_plan_bind.go +++ b/bundle/direct/bundle_plan_bind.go @@ -208,10 +208,8 @@ func (b *DeploymentBundle) handleBindPlan(ctx context.Context, resourceKey strin return true } -// buildBindConflictError formats a multi-line message that names the fields whose -// changes forced an incompatible action (Recreate / UpdateWithID) and shows the two -// concrete resolutions: drop the offending fields, or drop lifecycle.bind. firstBind -// tells a resource being adopted apart from one that is already bound in state. +// buildBindConflictError names the incompatible fields and distinguishes creating a +// new resource before adoption from replacing an already-managed resource. func buildBindConflictError(resourceKey string, action deployplan.ActionType, changes deployplan.Changes, firstBind bool) error { problematicFields := make([]string, 0, len(changes)) for fieldPath, change := range changes { @@ -219,39 +217,25 @@ func buildBindConflictError(resourceKey string, action deployplan.ActionType, ch problematicFields = append(problematicFields, fieldPath) } } - // Stable order so the YAML examples are deterministic across runs. slices.Sort(problematicFields) - resourceType, resourceName := splitResourceKey(resourceKey) - - var headline, effect, option1 string + var headline string // buildBindConflictError is only invoked for Recreate / UpdateWithID; other // action types are guarded by the caller. switch { //nolint:exhaustive case action == deployplan.Recreate && firstBind: headline = "cannot bind " + resourceKey + ": cannot recreate resource with lifecycle.bind" - effect = "This would destroy and recreate the existing workspace resource, changing its ID." case action == deployplan.Recreate: headline = "cannot plan " + resourceKey + ": lifecycle.bind prevents recreating the bound resource" - effect = "This would destroy and recreate the bound workspace resource, changing its ID." case action == deployplan.UpdateWithID && firstBind: headline = "cannot bind " + resourceKey + ": cannot update resource ID with lifecycle.bind" - effect = "This would replace the existing workspace resource with a new ID." case action == deployplan.UpdateWithID: headline = "cannot plan " + resourceKey + ": lifecycle.bind prevents changing the ID of the bound resource" - effect = "This would replace the bound workspace resource with a new ID." - } - if firstBind { - option1 = "Remove the problematic fields from your configuration to make this a bind-only operation:" - } else { - option1 = "Revert the problematic fields in your configuration to keep the bound resource as it is:" } var msg strings.Builder msg.WriteString(headline + "\n\n") - msg.WriteString(effect + "\n\n") - - msg.WriteString("The following fields cannot be modified because they require ") + msg.WriteString("Fields requiring ") if action == deployplan.Recreate { msg.WriteString("resource recreation:\n") } else { @@ -264,27 +248,12 @@ func buildBindConflictError(resourceKey string, action deployplan.ActionType, ch fmt.Fprintf(&msg, " - %s\n", field) } } - msg.WriteString("\n") - - msg.WriteString("To resolve this issue, you have two options:\n\n") - msg.WriteString("1. " + option1 + "\n\n") - msg.WriteString(" resources:\n") - fmt.Fprintf(&msg, " %s:\n", resourceType) - fmt.Fprintf(&msg, " %s:\n", resourceName) - msg.WriteString(" # Remove or comment out these fields:\n") - for _, field := range problematicFields { - fmt.Fprintf(&msg, " # %s: ...\n", field) + msg.WriteString("\nMatch these fields to the existing resource to preserve its ID") + if firstBind { + msg.WriteString("; remove lifecycle.bind to create a new resource instead of adopting this one") + } else { + msg.WriteString("; remove lifecycle.bind to allow the ID to change") } - msg.WriteString("\n") - msg.WriteString("2. Remove lifecycle.bind if you want to allow the resource to be recreated/updated:\n\n") - msg.WriteString(" resources:\n") - fmt.Fprintf(&msg, " %s:\n", resourceType) - fmt.Fprintf(&msg, " %s:\n", resourceName) - msg.WriteString(" lifecycle:\n") - msg.WriteString(" # Remove the bind block:\n") - msg.WriteString(" # bind:\n") - // No trailing newline: error strings must not end with one. - msg.WriteString(" # id: ") return errors.New(msg.String()) } diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index 18e7cf91ea7..fef226abe99 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -51,6 +51,16 @@ func Bind(ctx context.Context, b *bundle.Bundle, opts *BindOptions, stateDesc *s // Direct engine: import into temp state, run plan, check for changes. resourceKey := fmt.Sprintf("resources.%s.%s", opts.Group, opts.ResourceKey) + resource, _, err := b.Config.Resources.ResolveKey(opts.Group + "." + opts.ResourceKey) + if err == nil && resource.GetLifecycle().GetBind() != nil { + bindPath := structpath.NewPath(nil, "resources", opts.Group, opts.ResourceKey, "lifecycle", "bind") + logdiag.LogDiag(ctx, diag.Diagnostic{ + Severity: diag.Warning, + Summary: resourceKey + ".lifecycle.bind already declares the resource ID; use 'bundle deploy' to bind it declaratively", + Locations: b.Config.GetLocationsOf(bindPath), + Paths: []*structpath.PathNode{bindPath}, + }) + } _, statePath := b.StateFilenameDirect(ctx) result, err := b.DeploymentBundle.Bind(ctx, b.WorkspaceClient(ctx), &b.Config, statePath, resourceKey, opts.ResourceId) From 1af55cb2c840ea9c0de2e11b4cf564ce9e54c376 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Fri, 6 Nov 2026 11:31:35 +0000 Subject: [PATCH 20/20] Model declarative binding as a separate deployment phase --- .../bundle/deploy/bind/basic/output.txt | 10 +- .../deploy/bind/bind-and-resize/output.txt | 9 +- .../deploy/bind/bind-and-update/output.txt | 9 +- .../deploy/bind/block-removed/output.txt | 3 +- .../bundle/deploy/bind/block-removed/script | 2 +- .../deploy/bind/destroy-allowed/output.txt | 3 +- .../bundle/deploy/bind/dev-mode/output.txt | 6 +- .../deploy/bind/gone-in-state/output.txt | 3 +- .../id-collision-different-type/output.txt | 2 - .../bundle/deploy/bind/int-id/output.txt | 6 +- .../bind/missing-after-bind/databricks.yml | 14 + .../bind/missing-after-bind/out.test.toml | 2 + .../deploy/bind/missing-after-bind/output.txt | 53 +++ .../deploy/bind/missing-after-bind/script | 27 ++ .../deploy/bind/missing-after-bind/test.toml | 3 + .../bundle/deploy/bind/pure-bind/output.txt | 29 +- .../bundle/deploy/bind/pure-bind/script | 8 +- .../bind/recorded-phases/databricks.yml.tmpl | 17 + .../deploy/bind/recorded-phases/out.test.toml | 3 + .../deploy/bind/recorded-phases/output.txt | 167 +++++++++ .../bind/recorded-phases/resources.dms.json | 8 + .../bundle/deploy/bind/recorded-phases/script | 28 ++ .../deploy/bind/recorded-phases/test.toml | 5 + .../bind/recreate-blocked-in-state/output.txt | 3 +- .../bind/recreate-blocked/databricks.yml | 2 +- .../deploy/bind/recreate-blocked/output.txt | 35 +- .../deploy/bind/recreate-blocked/script | 9 + .../deploy/bind/ref-to-bound/output.txt | 11 +- .../bind/removed-from-config/output.txt | 3 +- .../deploy/bind/resource-not-found/output.txt | 2 - .../saved-plan-recreate/change_storage.py | 1 + .../bind/saved-plan-recreate/databricks.yml | 18 + .../bind/saved-plan-recreate/out.test.toml | 2 + .../bind/saved-plan-recreate/output.txt | 45 +++ .../deploy/bind/saved-plan-recreate/script | 21 ++ .../deploy/bind/saved-plan-recreate/test.toml | 4 + .../deploy/bind/saved-plan/databricks.yml | 22 ++ .../deploy/bind/saved-plan/out.test.toml | 2 + .../bundle/deploy/bind/saved-plan/output.txt | 77 +++++ .../bundle/deploy/bind/saved-plan/script | 31 ++ .../bundle/deploy/bind/saved-plan/test.toml | 2 + .../deploy/bind/selected/databricks.yml | 27 ++ .../bundle/deploy/bind/selected/out.test.toml | 2 + .../bundle/deploy/bind/selected/output.txt | 61 ++++ acceptance/bundle/deploy/bind/selected/script | 24 ++ .../bundle/deploy/bind/selected/test.toml | 1 + .../deploy/bind/target-override/output.txt | 6 +- .../bundle/deploy/bind/variable-id/output.txt | 9 +- .../readplan/cli-version-mismatch/plan.json | 2 +- .../readplan/lineage-mismatch/plan.dms.json | 2 +- .../readplan/lineage-mismatch/plan.json | 2 +- .../readplan/plan-version-mismatch/output.txt | 2 +- acceptance/test.toml | 4 +- bundle/deployplan/action.go | 56 ++-- bundle/deployplan/action_test.go | 14 +- bundle/deployplan/plan.go | 34 +- bundle/deployplan/plan_test.go | 28 ++ bundle/direct/apply.go | 70 +--- bundle/direct/bind_test.go | 93 ++--- bundle/direct/bundle_apply.go | 15 +- bundle/direct/bundle_plan.go | 50 ++- bundle/direct/bundle_plan_bind.go | 317 +++++++++++++----- bundle/direct/bundle_plan_bind_test.go | 76 +++++ bundle/direct/dstate/bind.go | 46 +++ bundle/direct/dstate/bind_test.go | 50 +++ bundle/internal/schema/annotations.yml | 6 +- bundle/phases/declarative_bind.go | 107 ++++++ bundle/phases/declarative_bind_test.go | 78 +++++ bundle/phases/deploy.go | 58 +++- bundle/phases/dms.go | 41 +-- bundle/schema/jsonschema.json | 6 +- cmd/bundle/plan.go | 23 +- .../bundles/alerts/_models/lifecycle.py | 4 +- .../apps/_models/lifecycle_with_started.py | 4 +- .../bundles/catalogs/_models/lifecycle.py | 4 +- .../cluster_policies/_models/lifecycle.py | 4 +- .../_models/lifecycle_with_started.py | 4 +- .../bundles/dashboards/_models/lifecycle.py | 4 +- .../_models/lifecycle.py | 4 +- .../database_catalogs/_models/lifecycle.py | 4 +- .../database_instances/_models/lifecycle.py | 4 +- .../bundles/experiments/_models/lifecycle.py | 4 +- .../external_locations/_models/lifecycle.py | 4 +- .../bundles/features/_models/lifecycle.py | 4 +- .../bundles/genie_spaces/_models/lifecycle.py | 4 +- .../instance_pools/_models/lifecycle.py | 4 +- .../job_runs/_models/job_run_lifecycle.py | 4 +- .../bundles/jobs/_models/lifecycle.py | 4 +- .../materialization_sets/_models/lifecycle.py | 4 +- .../bundles/mcp_services/_models/lifecycle.py | 4 +- .../_models/lifecycle.py | 4 +- .../model_services/_models/lifecycle.py | 4 +- .../_models/lifecycle.py | 4 +- .../bundles/models/_models/lifecycle.py | 4 +- .../bundles/pipelines/_models/lifecycle.py | 4 +- .../quality_monitors/_models/lifecycle.py | 4 +- .../registered_models/_models/lifecycle.py | 4 +- .../bundles/schemas/_models/lifecycle.py | 4 +- .../secret_scopes/_models/lifecycle.py | 4 +- .../bundles/secrets/_models/lifecycle.py | 4 +- .../_models/lifecycle_with_started.py | 4 +- .../_models/lifecycle.py | 4 +- .../_models/lifecycle.py | 4 +- .../_models/lifecycle.py | 4 +- .../bundles/volumes/_models/lifecycle.py | 4 +- 105 files changed, 1693 insertions(+), 456 deletions(-) create mode 100644 acceptance/bundle/deploy/bind/missing-after-bind/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/missing-after-bind/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/missing-after-bind/output.txt create mode 100644 acceptance/bundle/deploy/bind/missing-after-bind/script create mode 100644 acceptance/bundle/deploy/bind/missing-after-bind/test.toml create mode 100644 acceptance/bundle/deploy/bind/recorded-phases/databricks.yml.tmpl create mode 100644 acceptance/bundle/deploy/bind/recorded-phases/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/recorded-phases/output.txt create mode 100644 acceptance/bundle/deploy/bind/recorded-phases/resources.dms.json create mode 100644 acceptance/bundle/deploy/bind/recorded-phases/script create mode 100644 acceptance/bundle/deploy/bind/recorded-phases/test.toml create mode 100644 acceptance/bundle/deploy/bind/saved-plan-recreate/change_storage.py create mode 100644 acceptance/bundle/deploy/bind/saved-plan-recreate/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/saved-plan-recreate/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/saved-plan-recreate/output.txt create mode 100644 acceptance/bundle/deploy/bind/saved-plan-recreate/script create mode 100644 acceptance/bundle/deploy/bind/saved-plan-recreate/test.toml create mode 100644 acceptance/bundle/deploy/bind/saved-plan/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/saved-plan/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/saved-plan/output.txt create mode 100644 acceptance/bundle/deploy/bind/saved-plan/script create mode 100644 acceptance/bundle/deploy/bind/saved-plan/test.toml create mode 100644 acceptance/bundle/deploy/bind/selected/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/selected/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/selected/output.txt create mode 100644 acceptance/bundle/deploy/bind/selected/script create mode 100644 acceptance/bundle/deploy/bind/selected/test.toml create mode 100644 bundle/direct/bundle_plan_bind_test.go create mode 100644 bundle/direct/dstate/bind.go create mode 100644 bundle/direct/dstate/bind_test.go create mode 100644 bundle/phases/declarative_bind.go create mode 100644 bundle/phases/declarative_bind_test.go diff --git a/acceptance/bundle/deploy/bind/basic/output.txt b/acceptance/bundle/deploy/bind/basic/output.txt index 05e482d68f3..68d4a22f609 100644 --- a/acceptance/bundle/deploy/bind/basic/output.txt +++ b/acceptance/bundle/deploy/bind/basic/output.txt @@ -1,14 +1,18 @@ >>> [CLI] bundle plan -bind experiments.foo (id: [NEW_EXPERIMENT_ID]) +Bind: + experiments.foo (id: [NEW_EXPERIMENT_ID]) -Plan: 0 to add, 0 to change, 0 to delete, 0 unchanged, 1 to bind +Deploy: + No changes + +Plan: 0 to add, 0 to change, 0 to delete, 1 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 experiments.foo Files: 1 uploaded, 0 deleted -Resources: 0 created, 0 changed, 0 deleted, 0 unchanged, 1 bound +Resources: 0 created, 0 changed, 0 deleted, 1 unchanged, 1 bound >>> [CLI] bundle plan Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged diff --git a/acceptance/bundle/deploy/bind/bind-and-resize/output.txt b/acceptance/bundle/deploy/bind/bind-and-resize/output.txt index 9fb0ff28f18..a65b550a8a1 100644 --- a/acceptance/bundle/deploy/bind/bind-and-resize/output.txt +++ b/acceptance/bundle/deploy/bind/bind-and-resize/output.txt @@ -4,13 +4,18 @@ json.num_workers = 2; >>> [CLI] bundle plan -bind_and_update clusters.test_cluster (id: [CLUSTER_ID]) +Bind: + clusters.test_cluster (id: [CLUSTER_ID]) + +Deploy: + resize clusters.test_cluster 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 +Bound clusters.test_cluster +Resized clusters.test_cluster Files: 2 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound diff --git a/acceptance/bundle/deploy/bind/bind-and-update/output.txt b/acceptance/bundle/deploy/bind/bind-and-update/output.txt index 202baea71d5..666d0226942 100644 --- a/acceptance/bundle/deploy/bind/bind-and-update/output.txt +++ b/acceptance/bundle/deploy/bind/bind-and-update/output.txt @@ -1,12 +1,17 @@ >>> [CLI] bundle plan -bind_and_update jobs.foo (id: [NEW_JOB_ID]) +Bind: + jobs.foo (id: [NEW_JOB_ID]) + +Deploy: + update jobs.foo 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 +Bound jobs.foo +Updated jobs.foo Files: 2 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound diff --git a/acceptance/bundle/deploy/bind/block-removed/output.txt b/acceptance/bundle/deploy/bind/block-removed/output.txt index c51a96dd573..9ede5d99f4f 100644 --- a/acceptance/bundle/deploy/bind/block-removed/output.txt +++ b/acceptance/bundle/deploy/bind/block-removed/output.txt @@ -1,7 +1,8 @@ >>> [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 +Bound pipelines.foo +Updated pipelines.foo Files: 3 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound diff --git a/acceptance/bundle/deploy/bind/block-removed/script b/acceptance/bundle/deploy/bind/block-removed/script index f70af112b3d..a2d229b2980 100644 --- a/acceptance/bundle/deploy/bind/block-removed/script +++ b/acceptance/bundle/deploy/bind/block-removed/script @@ -15,7 +15,7 @@ trace $CLI bundle deploy --auto-approve trace print_state.py | gron.py | grep __id__ | contains.py "$NEW_PIPELINE_ID" title "Without the bind block, a storage change plans a recreate" -update_file.py databricks.yml "/old/storage/path" "/new/storage/path" +update_file.py databricks.yml "storage: /old/storage/path" "storage: /new/storage/path" trace $CLI bundle plan # Cleanup diff --git a/acceptance/bundle/deploy/bind/destroy-allowed/output.txt b/acceptance/bundle/deploy/bind/destroy-allowed/output.txt index 88283729e0a..3dd7baaa8d8 100644 --- a/acceptance/bundle/deploy/bind/destroy-allowed/output.txt +++ b/acceptance/bundle/deploy/bind/destroy-allowed/output.txt @@ -1,7 +1,8 @@ >>> [CLI] bundle deploy --auto-approve Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-destroy-allowed-[UNIQUE_NAME]/default/files... -Bound and updated jobs.foo +Bound jobs.foo +Updated jobs.foo Files: 2 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound diff --git a/acceptance/bundle/deploy/bind/dev-mode/output.txt b/acceptance/bundle/deploy/bind/dev-mode/output.txt index 0cd894c31d8..60e446adb60 100644 --- a/acceptance/bundle/deploy/bind/dev-mode/output.txt +++ b/acceptance/bundle/deploy/bind/dev-mode/output.txt @@ -1,6 +1,10 @@ >>> [CLI] bundle plan -bind_and_update jobs.foo (id: [NEW_JOB_ID]) +Bind: + jobs.foo (id: [NEW_JOB_ID]) + +Deploy: + update jobs.foo Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged, 1 to bind diff --git a/acceptance/bundle/deploy/bind/gone-in-state/output.txt b/acceptance/bundle/deploy/bind/gone-in-state/output.txt index bfebd09896d..a9cb132d9ba 100644 --- a/acceptance/bundle/deploy/bind/gone-in-state/output.txt +++ b/acceptance/bundle/deploy/bind/gone-in-state/output.txt @@ -1,7 +1,8 @@ >>> [CLI] bundle deploy --auto-approve Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-gone-in-state-[UNIQUE_NAME]/default/files... -Bound and updated jobs.foo +Bound jobs.foo +Updated jobs.foo Files: 1 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound diff --git a/acceptance/bundle/deploy/bind/id-collision-different-type/output.txt b/acceptance/bundle/deploy/bind/id-collision-different-type/output.txt index 4ef65c527d3..1d9c5cf80df 100644 --- a/acceptance/bundle/deploy/bind/id-collision-different-type/output.txt +++ b/acceptance/bundle/deploy/bind/id-collision-different-type/output.txt @@ -8,8 +8,6 @@ Resources: 1 created, 0 changed, 0 deleted, 0 unchanged >>> musterr [CLI] bundle plan Error: cannot bind resources.experiments.exp: resource with ID "[FOO_ID]" does not exist in workspace -Error: planning failed - >>> [CLI] bundle destroy --auto-approve The following resources will be deleted: diff --git a/acceptance/bundle/deploy/bind/int-id/output.txt b/acceptance/bundle/deploy/bind/int-id/output.txt index 06ee8a67f28..72a4123aefd 100644 --- a/acceptance/bundle/deploy/bind/int-id/output.txt +++ b/acceptance/bundle/deploy/bind/int-id/output.txt @@ -1,5 +1,9 @@ >>> [CLI] bundle plan -bind_and_update jobs.foo (id: [NEW_JOB_ID]) +Bind: + jobs.foo (id: [NEW_JOB_ID]) + +Deploy: + update jobs.foo Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged, 1 to bind diff --git a/acceptance/bundle/deploy/bind/missing-after-bind/databricks.yml b/acceptance/bundle/deploy/bind/missing-after-bind/databricks.yml new file mode 100644 index 00000000000..58094ef1fcb --- /dev/null +++ b/acceptance/bundle/deploy/bind/missing-after-bind/databricks.yml @@ -0,0 +1,14 @@ +bundle: + name: test-missing-after-bind-UNIQUE_NAME + +variables: + experiment_id: + description: Existing experiment to bind + +resources: + experiments: + existing: + name: /Shared/test-missing-after-bind-UNIQUE_NAME + lifecycle: + bind: + id: ${var.experiment_id} diff --git a/acceptance/bundle/deploy/bind/missing-after-bind/out.test.toml b/acceptance/bundle/deploy/bind/missing-after-bind/out.test.toml new file mode 100644 index 00000000000..a927a5fbc06 --- /dev/null +++ b/acceptance/bundle/deploy/bind/missing-after-bind/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/missing-after-bind/output.txt b/acceptance/bundle/deploy/bind/missing-after-bind/output.txt new file mode 100644 index 00000000000..125ce7a8845 --- /dev/null +++ b/acceptance/bundle/deploy/bind/missing-after-bind/output.txt @@ -0,0 +1,53 @@ + +=== Missing after binding: fresh deployment +>>> musterr [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-missing-after-bind-[UNIQUE_NAME]/default/files... +Bound experiments.existing +Error: cannot deploy resources.experiments.existing: bound resource with ID "[EXPERIMENT_ID]" no longer exists in the workspace; remove lifecycle.bind to create a new resource + +Files: 2 uploaded, 0 deleted + +>>> print_state.py +json.state.resources.experiments.existing.__id__ = "[EXPERIMENT_ID]"; + +=== The failed deployment has already uploaded its binding +>>> [CLI] bundle summary -o json +json.resources.experiments.existing.id = "[EXPERIMENT_ID]"; + +>>> [CLI] bundle deployment unbind existing +Warning: resources.experiments.existing.lifecycle.bind re-binds the resource on the next deploy; remove it to keep the resource unbound + at resources.experiments.existing.lifecycle.bind + in databricks.yml:14:11 + + +=== Missing after binding: saved deployment plan with a skip action +>>> [CLI] bundle plan -o json + +>>> jq .plan | with_entries(.value |= {action, prevent_id_change}) tmp.plan.json +{ + "resources.experiments.existing": { + "action": "skip", + "prevent_id_change": true + } +} + +>>> musterr [CLI] bundle deploy --plan tmp.plan.json --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-missing-after-bind-[UNIQUE_NAME]/default/files... +Bound experiments.existing +Error: cannot deploy resources.experiments.existing: bound resource with ID "[EXPERIMENT_ID]" no longer exists in the workspace; remove lifecycle.bind to create a new resource + +Files: 1 uploaded, 0 deleted + +>>> print_state.py +json.state.resources.experiments.existing.__id__ = "[EXPERIMENT_ID]"; + +=== Neither failed deploy creates or deletes an experiment +>>> print_requests.py //api/2.0/mlflow/experiments/create //api/2.0/mlflow/experiments/delete + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.experiments.existing + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-missing-after-bind-[UNIQUE_NAME]/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/missing-after-bind/script b/acceptance/bundle/deploy/bind/missing-after-bind/script new file mode 100644 index 00000000000..baaf04f2abb --- /dev/null +++ b/acceptance/bundle/deploy/bind/missing-after-bind/script @@ -0,0 +1,27 @@ +update_file.py databricks.yml UNIQUE_NAME "$UNIQUE_NAME" +EXPERIMENT_ID=$(MSYS_NO_PATHCONV=1 $CLI experiments create-experiment "/Shared/test-missing-after-bind-$UNIQUE_NAME" | jq -r .experiment_id) +add_repl "$EXPERIMENT_ID" EXPERIMENT_ID +export BUNDLE_VAR_experiment_id=$EXPERIMENT_ID +print_requests.py //api/2.0/mlflow/experiments/create > LOG.setup + +title "Missing after binding: fresh deployment" +fault.py 'GET /api/2.0/mlflow/experiments/get' 404 2 1 RESOURCE_DOES_NOT_EXIST +trace musterr $CLI bundle deploy --auto-approve +trace print_state.py | gron.py | grep __id__ + +title "The failed deployment has already uploaded its binding" +rm .databricks/bundle/default/resources.json +trace $CLI bundle summary -o json | gron.py | grep 'experiments.existing.id' +trace $CLI bundle deployment unbind existing + +title "Missing after binding: saved deployment plan with a skip action" +trace $CLI bundle plan -o json > tmp.plan.json +trace jq '.plan | with_entries(.value |= {action, prevent_id_change})' tmp.plan.json +fault.py 'GET /api/2.0/mlflow/experiments/get' 404 1 1 RESOURCE_DOES_NOT_EXIST +trace musterr $CLI bundle deploy --plan tmp.plan.json --auto-approve +trace print_state.py | gron.py | grep __id__ + +title "Neither failed deploy creates or deletes an experiment" +trace print_requests.py //api/2.0/mlflow/experiments/create //api/2.0/mlflow/experiments/delete | contains.py '!POST' +trace $CLI bundle destroy --auto-approve +rm "$OUT_REQUESTS" diff --git a/acceptance/bundle/deploy/bind/missing-after-bind/test.toml b/acceptance/bundle/deploy/bind/missing-after-bind/test.toml new file mode 100644 index 00000000000..642b257795e --- /dev/null +++ b/acceptance/bundle/deploy/bind/missing-after-bind/test.toml @@ -0,0 +1,3 @@ +Cloud = false +RecordRequests = true +Ignore = ["tmp.*.json"] diff --git a/acceptance/bundle/deploy/bind/pure-bind/output.txt b/acceptance/bundle/deploy/bind/pure-bind/output.txt index 7a98a5bbe13..aa7c1be7a20 100644 --- a/acceptance/bundle/deploy/bind/pure-bind/output.txt +++ b/acceptance/bundle/deploy/bind/pure-bind/output.txt @@ -1,8 +1,9 @@ >>> [CLI] bundle deploy --auto-approve Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-pure-[UNIQUE_NAME]/default/files... -Bound and updated jobs.foo +Bound jobs.foo Created jobs.foo.permissions +Updated jobs.foo Files: 1 uploaded, 0 deleted Resources: 1 created, 1 changed, 0 deleted, 0 unchanged, 1 bound @@ -30,14 +31,19 @@ Warning: resources.jobs.foo.lifecycle.bind re-binds the resource on the next dep === The job now matches the config, so this is a pure bind >>> [CLI] bundle plan -bind jobs.foo (id: [NEW_JOB_ID]) -create jobs.foo.permissions +Bind: + jobs.foo (id: [NEW_JOB_ID]) + +Deploy: + create jobs.foo.permissions -Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged, 1 to bind +Plan: 1 to add, 0 to change, 0 to delete, 1 unchanged, 1 to bind >>> [CLI] bundle plan -o json -json.plan.resources.jobs.foo.bind_id = "[NEW_JOB_ID]"; -json.plan.resources.jobs.foo.action = "bind"; +json.bind.resources.jobs.foo.id = "[NEW_JOB_ID]"; +json.plan.resources.jobs.foo.id = "[NEW_JOB_ID]"; +json.plan.resources.jobs.foo.action = "skip"; +json.plan.resources.jobs.foo.permissions.depends_on[0].label = "${resources.jobs.foo.id}"; >>> print_requests.py --get --oneline //jobs/get {"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} @@ -47,17 +53,24 @@ json.plan.resources.jobs.foo.action = "bind"; {"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} {"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} {"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} >>> [CLI] bundle deploy --auto-approve Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-pure-[UNIQUE_NAME]/default/files... Bound jobs.foo Created jobs.foo.permissions Files: 0 uploaded, 0 deleted -Resources: 1 created, 0 changed, 0 deleted, 0 unchanged, 1 bound +Resources: 1 created, 0 changed, 0 deleted, 1 unchanged, 1 bound -=== The re-bind deploy reads the job once, at plan time +=== Binding and deployment both verify that the job exists >>> print_requests.py --get --oneline //jobs/get {"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} +{"method": "GET", "path": "/api/2.2/jobs/get", "q": {"job_id": "[NEW_JOB_ID]"}} >>> print_state.py json.state.resources.jobs.foo.__id__ = "[NEW_JOB_ID]"; diff --git a/acceptance/bundle/deploy/bind/pure-bind/script b/acceptance/bundle/deploy/bind/pure-bind/script index b2b56968098..1cc6103013a 100644 --- a/acceptance/bundle/deploy/bind/pure-bind/script +++ b/acceptance/bundle/deploy/bind/pure-bind/script @@ -1,7 +1,7 @@ update_file.py databricks.yml 'UNIQUE_NAME' "$UNIQUE_NAME" -# Create a job in the workspace and bind it; the first deploy is a bind_and_update -# because the bundle sets fields such as deployment and edit_mode on every job. +# Create a job in the workspace and bind it; deployment then updates fields such +# as deployment and edit_mode that the bundle sets on every job. NEW_JOB_ID=$($CLI jobs create --json '{"name": "test-bind-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_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" @@ -17,12 +17,12 @@ trace $CLI bundle deployment unbind foo title "The job now matches the config, so this is a pure bind" trace $CLI bundle plan -trace $CLI bundle plan -o json | gron.py | grep -E 'jobs\.foo\.(action|bind_id)' +trace $CLI bundle plan -o json | gron.py | grep -E 'jobs\.foo\.(action|id)' trace print_requests.py --get --oneline //jobs/get trace $CLI bundle deploy --auto-approve -title "The re-bind deploy reads the job once, at plan time" +title "Binding and deployment both verify that the job exists" trace print_requests.py --get --oneline //jobs/get trace print_state.py | gron.py | grep __id__ diff --git a/acceptance/bundle/deploy/bind/recorded-phases/databricks.yml.tmpl b/acceptance/bundle/deploy/bind/recorded-phases/databricks.yml.tmpl new file mode 100644 index 00000000000..e1eb9693eb8 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recorded-phases/databricks.yml.tmpl @@ -0,0 +1,17 @@ +bundle: + name: test-bind-phases-$UNIQUE_NAME + +experimental: + deployment_history: true + +variables: + job_id: + description: Existing job + +resources: + jobs: + existing: + name: after-bind + lifecycle: + bind: + id: ${var.job_id} diff --git a/acceptance/bundle/deploy/bind/recorded-phases/out.test.toml b/acceptance/bundle/deploy/bind/recorded-phases/out.test.toml new file mode 100644 index 00000000000..c51c6ac1c26 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recorded-phases/out.test.toml @@ -0,0 +1,3 @@ +Cloud = false +EnvMatrix.DMS = ["true"] +EnvMatrix.MODE = ["shadow", "authoritative"] diff --git a/acceptance/bundle/deploy/bind/recorded-phases/output.txt b/acceptance/bundle/deploy/bind/recorded-phases/output.txt new file mode 100644 index 00000000000..160bc31e974 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recorded-phases/output.txt @@ -0,0 +1,167 @@ + +=== Planning neither records a version nor binds a resource +>>> [CLI] bundle plan -o json + +>>> print_requests.py --dms //api/2.0/bundle + +=== Bind completes before a failing apply starts its own version +>>> musterr [CLI] bundle deploy --plan tmp.plan.json --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-phases-[UNIQUE_NAME]/default/files... +Bound jobs.existing +Error: cannot update resources.jobs.existing: updating id=[JOB_ID]: Fault injected by test. (400 INVALID_PARAMETER_VALUE) + +Endpoint: POST [DATABRICKS_URL]/api/2.2/jobs/reset +HTTP Status: 400 Bad Request +API error_code: INVALID_PARAMETER_VALUE +API message: Fault injected by test. + +Files: 4 uploaded, 0 deleted + +>>> print_state.py +json.state.resources.jobs.existing.__id__ = "[JOB_ID]"; +json.state.resources.jobs.existing.state.name = "before-bind"; + +>>> print_requests.py --dms --keep --method PATCH //api/2.0/bundle/deployments ^//versions + +>>> print_requests.py --dms //versions //api/2.2/jobs/reset +{ + "method": "POST", + "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", + "version": "1", + "operations": [ + { + "action_type": "OPERATION_ACTION_TYPE_BIND", + "resource_key": "jobs.existing" + } + ], + "status": null, + "completion_reason": null, + "state_name": null +} +{ + "method": "PATCH", + "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/1/operations/jobs.existing", + "version": null, + "operations": null, + "status": "OPERATION_STATUS_SUCCEEDED", + "completion_reason": null, + "state_name": "before-bind" +} +{ + "method": "POST", + "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/1/complete", + "version": null, + "operations": null, + "status": null, + "completion_reason": "VERSION_COMPLETE_SUCCESS", + "state_name": null +} +{ + "method": "POST", + "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", + "version": "2", + "operations": [ + { + "action_type": "OPERATION_ACTION_TYPE_UPDATE", + "resource_key": "jobs.existing" + } + ], + "status": null, + "completion_reason": null, + "state_name": null +} +{ + "method": "POST", + "path": "/api/2.2/jobs/reset", + "version": null, + "operations": null, + "status": null, + "completion_reason": null, + "state_name": null +} +{ + "method": "PATCH", + "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/2/operations/jobs.existing", + "version": null, + "operations": null, + "status": "OPERATION_STATUS_FAILED", + "completion_reason": null, + "state_name": "before-bind" +} +{ + "method": "POST", + "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/2/complete", + "version": null, + "operations": null, + "status": null, + "completion_reason": "VERSION_COMPLETE_FAILURE", + "state_name": null +} + +=== Retrying plans only the unapplied update +>>> [CLI] bundle plan -o json + +>>> jq {has_bind: has("bind"), action: .plan["resources.jobs.existing"].action} tmp.retry.json +{ + "has_bind": false, + "action": "update" +} + +>>> [CLI] bundle deploy --plan tmp.retry.json --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-phases-[UNIQUE_NAME]/default/files... +Updated jobs.existing +Files: 1 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged + +>>> [CLI] jobs get [JOB_ID] +{ + "name": "after-bind", + "version": "3" +} + +>>> print_requests.py --dms //versions //api/2.2/jobs/reset +{ + "method": "POST", + "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", + "version": "3", + "operations": [ + { + "action_type": "OPERATION_ACTION_TYPE_UPDATE", + "resource_key": "jobs.existing" + } + ], + "status": null, + "completion_reason": null +} +{ + "method": "POST", + "path": "/api/2.2/jobs/reset", + "version": null, + "operations": null, + "status": null, + "completion_reason": null +} +{ + "method": "PATCH", + "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/3/operations/jobs.existing", + "version": null, + "operations": null, + "status": "OPERATION_STATUS_SUCCEEDED", + "completion_reason": null +} +{ + "method": "POST", + "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/3/complete", + "version": null, + "operations": null, + "status": null, + "completion_reason": "VERSION_COMPLETE_SUCCESS" +} + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.jobs.existing + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-phases-[UNIQUE_NAME]/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/recorded-phases/resources.dms.json b/acceptance/bundle/deploy/bind/recorded-phases/resources.dms.json new file mode 100644 index 00000000000..bf68eb50168 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recorded-phases/resources.dms.json @@ -0,0 +1,8 @@ +{ + "state_version": 3, + "features": {"deployment_history": {}}, + "cli_version": "0.0.0-dev", + "lineage": "test-lineage", + "serial": 0, + "state": {} +} diff --git a/acceptance/bundle/deploy/bind/recorded-phases/script b/acceptance/bundle/deploy/bind/recorded-phases/script new file mode 100644 index 00000000000..8d67dc15d4d --- /dev/null +++ b/acceptance/bundle/deploy/bind/recorded-phases/script @@ -0,0 +1,28 @@ +envsubst < databricks.yml.tmpl > databricks.yml +JOB_ID=$($CLI jobs create --json '{"name":"before-bind"}' | jq -r .job_id) +add_repl "$JOB_ID" JOB_ID +export BUNDLE_VAR_job_id=$JOB_ID +if [[ "$MODE" == authoritative ]]; then + mkdir -p .databricks/bundle/default + cp resources.dms.json .databricks/bundle/default/resources.json +fi + +title "Planning neither records a version nor binds a resource" +trace $CLI bundle plan -o json > tmp.plan.json +trace print_requests.py --dms //api/2.0/bundle + +title "Bind completes before a failing apply starts its own version" +fault.py 'POST /api/2.2/jobs/reset' 400 0 1 INVALID_PARAMETER_VALUE +trace musterr $CLI bundle deploy --plan tmp.plan.json --auto-approve +trace print_state.py | gron.py | grep -E '__id__|\.name =' +trace print_requests.py --dms --keep --method PATCH //api/2.0/bundle/deployments ^//versions +trace print_requests.py --dms //versions //api/2.2/jobs/reset | jq '{method, path, version: .q.version_id, operations: .body.operations, status: .body.status, completion_reason: .body.completion_reason, state_name: (if .body.state then (.body.state | fromjson | .state.name) else null end)}' + +title "Retrying plans only the unapplied update" +trace $CLI bundle plan -o json > tmp.retry.json +trace jq '{has_bind: has("bind"), action: .plan["resources.jobs.existing"].action}' tmp.retry.json +trace $CLI bundle deploy --plan tmp.retry.json --auto-approve +trace $CLI jobs get "$JOB_ID" | jq '.settings | {name, version: .deployment.version_id}' +trace print_requests.py --dms //versions //api/2.2/jobs/reset | jq '{method, path, version: .q.version_id, operations: .body.operations, status: .body.status, completion_reason: .body.completion_reason}' +trace $CLI bundle destroy --auto-approve +rm "$OUT_REQUESTS" diff --git a/acceptance/bundle/deploy/bind/recorded-phases/test.toml b/acceptance/bundle/deploy/bind/recorded-phases/test.toml new file mode 100644 index 00000000000..813ce95ace0 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recorded-phases/test.toml @@ -0,0 +1,5 @@ +Cloud = false +RecordRequests = true +Ignore = ["databricks.yml", "tmp.*.json"] +EnvMatrix.DMS = ["true"] +EnvMatrix.MODE = ["shadow", "authoritative"] diff --git a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt index 34f11e65065..b0cb58be5a6 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/output.txt @@ -1,7 +1,8 @@ >>> [CLI] bundle deploy --auto-approve Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-recreate-in-state-[UNIQUE_NAME]/default/files... -Bound and updated pipelines.foo +Bound pipelines.foo +Updated pipelines.foo Files: 2 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound diff --git a/acceptance/bundle/deploy/bind/recreate-blocked/databricks.yml b/acceptance/bundle/deploy/bind/recreate-blocked/databricks.yml index 7afeed66ed2..99fa3d63fc0 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked/databricks.yml +++ b/acceptance/bundle/deploy/bind/recreate-blocked/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-recreate + name: test-bind-recreate-UNIQUE_NAME resources: pipelines: diff --git a/acceptance/bundle/deploy/bind/recreate-blocked/output.txt b/acceptance/bundle/deploy/bind/recreate-blocked/output.txt index 31e087ae7a0..d64d64d6bb6 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked/output.txt +++ b/acceptance/bundle/deploy/bind/recreate-blocked/output.txt @@ -1,11 +1,42 @@ >>> musterr [CLI] bundle plan -Error: cannot bind resources.pipelines.foo: cannot recreate resource with lifecycle.bind +Error: cannot plan resources.pipelines.foo: lifecycle.bind prevents recreating the bound resource Fields requiring resource recreation: - storage (immutable) -Match these fields to the existing resource to preserve its ID; remove lifecycle.bind to create a new resource instead of adopting this one +Match these fields to the existing resource to preserve its ID; remove lifecycle.bind to allow the ID to change Error: planning failed + +=== Deploy commits the bind phase before rejecting recreation +>>> musterr [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-recreate-[UNIQUE_NAME]/default/files... +Bound pipelines.foo +Error: cannot plan resources.pipelines.foo: lifecycle.bind prevents recreating the bound resource + +Fields requiring resource recreation: + - storage (immutable) + +Match these fields to the existing resource to preserve its ID; remove lifecycle.bind to allow the ID to change + +Error: planning failed + +Files: 2 uploaded, 0 deleted + +>>> print_state.py +json.state.resources.pipelines.foo.__id__ = "[NEW_PIPELINE_ID]"; + +=== Destroy still deletes the adopted resource +>>> [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-recreate-[UNIQUE_NAME]/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/recreate-blocked/script b/acceptance/bundle/deploy/bind/recreate-blocked/script index f28f6b94ac8..192af0ca4ee 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked/script +++ b/acceptance/bundle/deploy/bind/recreate-blocked/script @@ -1,3 +1,5 @@ +update_file.py databricks.yml UNIQUE_NAME "$UNIQUE_NAME" + # Create a pipeline with a different storage path 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 @@ -7,3 +9,10 @@ update_file.py databricks.yml 'PLACEHOLDER_PIPELINE_ID' "$NEW_PIPELINE_ID" # Run plan - should fail because changing storage requires recreate which is blocked for binds trace musterr $CLI bundle plan + +title "Deploy commits the bind phase before rejecting recreation" +trace musterr $CLI bundle deploy --auto-approve +trace print_state.py | gron.py | grep __id__ + +title "Destroy still deletes the adopted resource" +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deploy/bind/ref-to-bound/output.txt b/acceptance/bundle/deploy/bind/ref-to-bound/output.txt index 10dcb40bbcb..2d74d5450d0 100644 --- a/acceptance/bundle/deploy/bind/ref-to-bound/output.txt +++ b/acceptance/bundle/deploy/bind/ref-to-bound/output.txt @@ -1,7 +1,11 @@ >>> [CLI] bundle plan -create jobs.bar -bind_and_update jobs.foo (id: [NEW_JOB_ID]) +Bind: + jobs.foo (id: [NEW_JOB_ID]) + +Deploy: + create jobs.bar + update jobs.foo Plan: 1 to add, 1 to change, 0 to delete, 0 unchanged, 1 to bind @@ -11,8 +15,9 @@ json.plan.resources.jobs.bar.new_state.value.tasks[0].run_job_task.job_id = [NEW >>> [CLI] bundle deploy --auto-approve Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-ref-to-bound-[UNIQUE_NAME]/default/files... -Bound and updated jobs.foo +Bound jobs.foo Created jobs.bar +Updated jobs.foo Files: 1 uploaded, 0 deleted Resources: 1 created, 1 changed, 0 deleted, 0 unchanged, 1 bound diff --git a/acceptance/bundle/deploy/bind/removed-from-config/output.txt b/acceptance/bundle/deploy/bind/removed-from-config/output.txt index c7e97a1b674..86f7351c6ff 100644 --- a/acceptance/bundle/deploy/bind/removed-from-config/output.txt +++ b/acceptance/bundle/deploy/bind/removed-from-config/output.txt @@ -1,7 +1,8 @@ >>> [CLI] bundle deploy --auto-approve Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-removed-from-config-[UNIQUE_NAME]/default/files... -Bound and updated jobs.foo +Bound jobs.foo +Updated jobs.foo Files: 2 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound diff --git a/acceptance/bundle/deploy/bind/resource-not-found/output.txt b/acceptance/bundle/deploy/bind/resource-not-found/output.txt index bf1e85e7591..ce68cb9da32 100644 --- a/acceptance/bundle/deploy/bind/resource-not-found/output.txt +++ b/acceptance/bundle/deploy/bind/resource-not-found/output.txt @@ -2,5 +2,3 @@ >>> musterr [CLI] bundle plan Error: cannot bind resources.jobs.foo: resource with ID "999999999" does not exist in workspace -Error: planning failed - diff --git a/acceptance/bundle/deploy/bind/saved-plan-recreate/change_storage.py b/acceptance/bundle/deploy/bind/saved-plan-recreate/change_storage.py new file mode 100644 index 00000000000..a439f346cbb --- /dev/null +++ b/acceptance/bundle/deploy/bind/saved-plan-recreate/change_storage.py @@ -0,0 +1 @@ +r["storage"] = "/changed/storage" # noqa: F821 - edit_resource.py provides r diff --git a/acceptance/bundle/deploy/bind/saved-plan-recreate/databricks.yml b/acceptance/bundle/deploy/bind/saved-plan-recreate/databricks.yml new file mode 100644 index 00000000000..a4b0eca9f48 --- /dev/null +++ b/acceptance/bundle/deploy/bind/saved-plan-recreate/databricks.yml @@ -0,0 +1,18 @@ +bundle: + name: test-bind-saved-recreate-UNIQUE_NAME + +variables: + pipeline_id: + description: Existing pipeline + +resources: + pipelines: + existing: + name: existing-pipeline + storage: /old/storage + libraries: + - notebook: + path: /Workspace/test + lifecycle: + bind: + id: ${var.pipeline_id} diff --git a/acceptance/bundle/deploy/bind/saved-plan-recreate/out.test.toml b/acceptance/bundle/deploy/bind/saved-plan-recreate/out.test.toml new file mode 100644 index 00000000000..a927a5fbc06 --- /dev/null +++ b/acceptance/bundle/deploy/bind/saved-plan-recreate/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/saved-plan-recreate/output.txt b/acceptance/bundle/deploy/bind/saved-plan-recreate/output.txt new file mode 100644 index 00000000000..7be6803315b --- /dev/null +++ b/acceptance/bundle/deploy/bind/saved-plan-recreate/output.txt @@ -0,0 +1,45 @@ + +=== Save a plan that preserves the resource +>>> [CLI] bundle plan -o json + +>>> jq .plan | with_entries(.value |= {action, prevent_id_change}) tmp.plan.json +{ + "resources.pipelines.existing": { + "action": "update", + "prevent_id_change": true + } +} + +=== Deploy binds, then rejects a newly required recreation +>>> musterr [CLI] bundle deploy --plan tmp.plan.json --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-saved-recreate-[UNIQUE_NAME]/default/files... +Bound pipelines.existing +Error: cannot deploy resources.pipelines.existing: lifecycle.bind prevents recreating the bound resource + +Fields requiring resource recreation: + - storage (immutable) + +Match these fields to the existing resource to preserve its ID; remove lifecycle.bind to allow the ID to change + +Files: 4 uploaded, 0 deleted + +>>> print_state.py +json.state.resources.pipelines.existing.__id__ = "[PIPELINE_ID]"; + +>>> print_requests.py //api/2.0/pipelines + +=== The committed binding survives pulling state again +>>> [CLI] bundle summary -o json +json.resources.pipelines.existing.id = "[PIPELINE_ID]"; + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.pipelines.existing + +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.existing + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-saved-recreate-[UNIQUE_NAME]/default + +Destroy: 1 deleted diff --git a/acceptance/bundle/deploy/bind/saved-plan-recreate/script b/acceptance/bundle/deploy/bind/saved-plan-recreate/script new file mode 100644 index 00000000000..e2560971799 --- /dev/null +++ b/acceptance/bundle/deploy/bind/saved-plan-recreate/script @@ -0,0 +1,21 @@ +update_file.py databricks.yml UNIQUE_NAME "$UNIQUE_NAME" +PIPELINE_ID=$($CLI pipelines create --json '{"name":"existing-pipeline","storage":"/old/storage","libraries":[{"notebook":{"path":"/Workspace/test"}}]}' | jq -r .pipeline_id) +add_repl "$PIPELINE_ID" PIPELINE_ID +export BUNDLE_VAR_pipeline_id=$PIPELINE_ID + +title "Save a plan that preserves the resource" +trace $CLI bundle plan -o json > tmp.plan.json +trace jq '.plan | with_entries(.value |= {action, prevent_id_change})' tmp.plan.json +edit_resource.py pipelines "$PIPELINE_ID" < change_storage.py +print_requests.py //api/2.0/pipelines > LOG.setup + +title "Deploy binds, then rejects a newly required recreation" +trace musterr $CLI bundle deploy --plan tmp.plan.json --auto-approve +trace print_state.py | gron.py | grep __id__ +trace print_requests.py //api/2.0/pipelines + +title "The committed binding survives pulling state again" +rm .databricks/bundle/default/resources.json +trace $CLI bundle summary -o json | gron.py | grep 'pipelines.existing.id' +trace $CLI bundle destroy --auto-approve +rm "$OUT_REQUESTS" diff --git a/acceptance/bundle/deploy/bind/saved-plan-recreate/test.toml b/acceptance/bundle/deploy/bind/saved-plan-recreate/test.toml new file mode 100644 index 00000000000..68b3b278dcb --- /dev/null +++ b/acceptance/bundle/deploy/bind/saved-plan-recreate/test.toml @@ -0,0 +1,4 @@ +# The fake server lets this test simulate an immutable field changing after planning. +Cloud = false +RecordRequests = true +Ignore = ["tmp.*.json"] diff --git a/acceptance/bundle/deploy/bind/saved-plan/databricks.yml b/acceptance/bundle/deploy/bind/saved-plan/databricks.yml new file mode 100644 index 00000000000..74df6045d8c --- /dev/null +++ b/acceptance/bundle/deploy/bind/saved-plan/databricks.yml @@ -0,0 +1,22 @@ +bundle: + name: test-bind-saved-plan-UNIQUE_NAME + +variables: + job_id: + description: Existing job to bind + experiment_id: + description: Existing experiment to bind + +resources: + jobs: + updated: + name: saved-plan-job + lifecycle: + bind: + id: ${var.job_id} + experiments: + unchanged: + name: /Shared/test-bind-saved-plan-UNIQUE_NAME + lifecycle: + bind: + id: ${var.experiment_id} diff --git a/acceptance/bundle/deploy/bind/saved-plan/out.test.toml b/acceptance/bundle/deploy/bind/saved-plan/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/saved-plan/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/saved-plan/output.txt b/acceptance/bundle/deploy/bind/saved-plan/output.txt new file mode 100644 index 00000000000..92c5610a67a --- /dev/null +++ b/acceptance/bundle/deploy/bind/saved-plan/output.txt @@ -0,0 +1,77 @@ + +=== The saved plan contains bindings and ordinary deployment actions +>>> [CLI] bundle plan -o json + +>>> jq {plan_version, bind: (.bind | with_entries(.value |= {id})), plan: (.plan | with_entries(.value |= {action, prevent_id_change}))} tmp.plan.json +{ + "plan_version": [PLAN_VERSION], + "bind": { + "resources.experiments.unchanged": { + "id": "[EXPERIMENT_ID]" + }, + "resources.jobs.updated": { + "id": "[JOB_ID]" + } + }, + "plan": { + "resources.experiments.unchanged": { + "action": "skip", + "prevent_id_change": true + }, + "resources.jobs.updated": { + "action": "update", + "prevent_id_change": true + } + } +} + +>>> find.py resources\.json$ --expect 0 + +>>> print_requests.py //api/2.0/workspace/import + +=== Deploy uses the saved IDs and configuration +>>> [CLI] bundle deploy --plan tmp.plan.json --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-saved-plan-[UNIQUE_NAME]/default/files... +Bound experiments.unchanged +Bound jobs.updated +Updated jobs.updated +Files: 2 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 1 unchanged, 2 bound + +>>> [CLI] jobs get [JOB_ID] +saved-plan-job + +>>> print_state.py +json.state.resources.experiments.unchanged.__id__ = "[EXPERIMENT_ID]"; +json.state.resources.jobs.updated.__id__ = "[JOB_ID]"; + +=== Reusing a plan after state has changed is rejected +>>> musterr [CLI] bundle deploy --plan tmp.plan.json --auto-approve +Error: plan lineage "" does not match state lineage "[UUID]"; the state may have been modified by another process + + +=== A subsequent plan has no bind phase +>>> [CLI] bundle plan -o json + +>>> jq {has_bind: has("bind"), actions: [.plan[].action]} tmp.next.json +{ + "has_bind": false, + "actions": [ + "skip", + "skip" + ] +} + +>>> [CLI] bundle deploy --plan tmp.next.json --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-saved-plan-[UNIQUE_NAME]/default/files... +Files: 2 uploaded, 0 deleted +Resources: 0 created, 0 changed, 0 deleted, 2 unchanged + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.experiments.unchanged + delete resources.jobs.updated + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-saved-plan-[UNIQUE_NAME]/default + +Destroy: 2 deleted diff --git a/acceptance/bundle/deploy/bind/saved-plan/script b/acceptance/bundle/deploy/bind/saved-plan/script new file mode 100644 index 00000000000..048aea82b49 --- /dev/null +++ b/acceptance/bundle/deploy/bind/saved-plan/script @@ -0,0 +1,31 @@ +update_file.py databricks.yml UNIQUE_NAME "$UNIQUE_NAME" +JOB_ID=$($CLI jobs create --json '{"name":"original-job"}' | jq -r .job_id) +EXPERIMENT_ID=$(MSYS_NO_PATHCONV=1 $CLI experiments create-experiment "/Shared/test-bind-saved-plan-$UNIQUE_NAME" | jq -r .experiment_id) +add_repl "$JOB_ID" JOB_ID +add_repl "$EXPERIMENT_ID" EXPERIMENT_ID +export BUNDLE_VAR_job_id=$JOB_ID BUNDLE_VAR_experiment_id=$EXPERIMENT_ID + +title "The saved plan contains bindings and ordinary deployment actions" +trace $CLI bundle plan -o json > tmp.plan.json +trace jq '{plan_version, bind: (.bind | with_entries(.value |= {id})), plan: (.plan | with_entries(.value |= {action, prevent_id_change}))}' tmp.plan.json +trace find.py 'resources\.json$' --expect 0 +trace print_requests.py //api/2.0/workspace/import + +title "Deploy uses the saved IDs and configuration" +update_file.py databricks.yml saved-plan-job ignored-local-name +export BUNDLE_VAR_job_id=not-the-planned-id +trace $CLI bundle deploy --plan tmp.plan.json --auto-approve +trace $CLI jobs get "$JOB_ID" | jq -r .settings.name +trace print_state.py | gron.py | grep __id__ + +title "Reusing a plan after state has changed is rejected" +trace musterr $CLI bundle deploy --plan tmp.plan.json --auto-approve + +title "A subsequent plan has no bind phase" +update_file.py databricks.yml ignored-local-name saved-plan-job +export BUNDLE_VAR_job_id=$JOB_ID +trace $CLI bundle plan -o json > tmp.next.json +trace jq '{has_bind: has("bind"), actions: [.plan[].action]}' tmp.next.json +trace $CLI bundle deploy --plan tmp.next.json --auto-approve +trace $CLI bundle destroy --auto-approve +rm "$OUT_REQUESTS" diff --git a/acceptance/bundle/deploy/bind/saved-plan/test.toml b/acceptance/bundle/deploy/bind/saved-plan/test.toml new file mode 100644 index 00000000000..c5daf70b466 --- /dev/null +++ b/acceptance/bundle/deploy/bind/saved-plan/test.toml @@ -0,0 +1,2 @@ +RecordRequests = true +Ignore = ["tmp.*.json"] diff --git a/acceptance/bundle/deploy/bind/selected/databricks.yml b/acceptance/bundle/deploy/bind/selected/databricks.yml new file mode 100644 index 00000000000..9f3c0a0f838 --- /dev/null +++ b/acceptance/bundle/deploy/bind/selected/databricks.yml @@ -0,0 +1,27 @@ +bundle: + name: test-bind-selected-UNIQUE_NAME + +variables: + parent_id: + description: Existing parent job + other_id: + description: Existing unrelated job + +resources: + jobs: + parent: + name: selected-parent + lifecycle: + bind: + id: ${var.parent_id} + child: + name: selected-child + tasks: + - task_key: parent + run_job_task: + job_id: ${resources.jobs.parent.id} + other: + name: unselected + lifecycle: + bind: + id: ${var.other_id} diff --git a/acceptance/bundle/deploy/bind/selected/out.test.toml b/acceptance/bundle/deploy/bind/selected/out.test.toml new file mode 100644 index 00000000000..0f167effef1 --- /dev/null +++ b/acceptance/bundle/deploy/bind/selected/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/deploy/bind/selected/output.txt b/acceptance/bundle/deploy/bind/selected/output.txt new file mode 100644 index 00000000000..0e71665205c --- /dev/null +++ b/acceptance/bundle/deploy/bind/selected/output.txt @@ -0,0 +1,61 @@ + +=== Selecting a child includes its binding dependency +>>> [CLI] bundle plan --select child -o json + +>>> jq {bind: (.bind | keys), plan: (.plan | keys), not_selected} tmp.plan.json +{ + "bind": [ + "resources.jobs.parent" + ], + "plan": [ + "resources.jobs.child", + "resources.jobs.parent" + ], + "not_selected": 1 +} + +=== Applying the selection leaves the unrelated resource unbound +>>> [CLI] bundle deploy --plan tmp.plan.json --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-selected-[UNIQUE_NAME]/default/files... +Bound jobs.parent +Created jobs.child +Updated jobs.parent +Files: 2 uploaded, 0 deleted +Resources: 1 created, 1 changed, 0 deleted, 0 unchanged, 1 bound, 1 not selected + +>>> print_state.py +json.state.resources.jobs.child.__id__ = "[CHILD_ID]"; +json.state.resources.jobs.parent.__id__ = "[PARENT_ID]"; + +=== A fresh selected deploy also leaves it unbound +>>> [CLI] bundle deploy --select child --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-selected-[UNIQUE_NAME]/default/files... +Files: 0 uploaded, 0 deleted +Resources: 0 created, 0 changed, 0 deleted, 2 unchanged, 1 not selected + +>>> print_state.py +json.state.resources.jobs.child.__id__ = "[CHILD_ID]"; +json.state.resources.jobs.parent.__id__ = "[PARENT_ID]"; + +=== The remaining binding can be applied separately +>>> [CLI] bundle deploy --select other --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-selected-[UNIQUE_NAME]/default/files... +Bound jobs.other +Updated jobs.other +Files: 0 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound, 2 not selected + +>>> print_state.py +json.state.resources.jobs.child.__id__ = "[CHILD_ID]"; +json.state.resources.jobs.other.__id__ = "[OTHER_ID]"; +json.state.resources.jobs.parent.__id__ = "[PARENT_ID]"; + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.jobs.child + delete resources.jobs.other + delete resources.jobs.parent + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-selected-[UNIQUE_NAME]/default + +Destroy: 3 deleted diff --git a/acceptance/bundle/deploy/bind/selected/script b/acceptance/bundle/deploy/bind/selected/script new file mode 100644 index 00000000000..24c18bed5bf --- /dev/null +++ b/acceptance/bundle/deploy/bind/selected/script @@ -0,0 +1,24 @@ +update_file.py databricks.yml UNIQUE_NAME "$UNIQUE_NAME" +PARENT_ID=$($CLI jobs create --json '{"name":"parent"}' | jq -r .job_id) +OTHER_ID=$($CLI jobs create --json '{"name":"other"}' | jq -r .job_id) +add_repl "$PARENT_ID" PARENT_ID +add_repl "$OTHER_ID" OTHER_ID +export BUNDLE_VAR_parent_id=$PARENT_ID BUNDLE_VAR_other_id=$OTHER_ID + +title "Selecting a child includes its binding dependency" +trace $CLI bundle plan --select child -o json > tmp.plan.json +trace jq '{bind: (.bind | keys), plan: (.plan | keys), not_selected}' tmp.plan.json + +title "Applying the selection leaves the unrelated resource unbound" +trace $CLI bundle deploy --plan tmp.plan.json --auto-approve +add_repl "$(read_id.py child)" CHILD_ID +trace print_state.py | gron.py | grep __id__ + +title "A fresh selected deploy also leaves it unbound" +trace $CLI bundle deploy --select child --auto-approve +trace print_state.py | gron.py | grep __id__ + +title "The remaining binding can be applied separately" +trace $CLI bundle deploy --select other --auto-approve +trace print_state.py | gron.py | grep __id__ +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deploy/bind/selected/test.toml b/acceptance/bundle/deploy/bind/selected/test.toml new file mode 100644 index 00000000000..751ceccb63d --- /dev/null +++ b/acceptance/bundle/deploy/bind/selected/test.toml @@ -0,0 +1 @@ +Ignore = ["tmp.*.json"] diff --git a/acceptance/bundle/deploy/bind/target-override/output.txt b/acceptance/bundle/deploy/bind/target-override/output.txt index 01b1ecff408..14da1960634 100644 --- a/acceptance/bundle/deploy/bind/target-override/output.txt +++ b/acceptance/bundle/deploy/bind/target-override/output.txt @@ -7,6 +7,10 @@ } >>> [CLI] bundle plan -bind_and_update jobs.foo (id: [NEW_JOB_ID]) +Bind: + jobs.foo (id: [NEW_JOB_ID]) + +Deploy: + update jobs.foo Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged, 1 to bind diff --git a/acceptance/bundle/deploy/bind/variable-id/output.txt b/acceptance/bundle/deploy/bind/variable-id/output.txt index 9aec503d1e0..4b5235fd3c4 100644 --- a/acceptance/bundle/deploy/bind/variable-id/output.txt +++ b/acceptance/bundle/deploy/bind/variable-id/output.txt @@ -1,12 +1,17 @@ >>> [CLI] bundle plan --var job_id=[NEW_JOB_ID] -bind_and_update jobs.foo (id: [NEW_JOB_ID]) +Bind: + jobs.foo (id: [NEW_JOB_ID]) + +Deploy: + update jobs.foo Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged, 1 to bind >>> [CLI] bundle deploy --auto-approve --var job_id=[NEW_JOB_ID] Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-variable-id-[UNIQUE_NAME]/default/files... -Bound and updated jobs.foo +Bound jobs.foo +Updated jobs.foo Files: 1 uploaded, 0 deleted Resources: 0 created, 1 changed, 0 deleted, 0 unchanged, 1 bound diff --git a/acceptance/bundle/deploy/readplan/cli-version-mismatch/plan.json b/acceptance/bundle/deploy/readplan/cli-version-mismatch/plan.json index 932edf74f84..bd9561c9ec8 100644 --- a/acceptance/bundle/deploy/readplan/cli-version-mismatch/plan.json +++ b/acceptance/bundle/deploy/readplan/cli-version-mismatch/plan.json @@ -1,5 +1,5 @@ { - "plan_version": 2, + "plan_version": 3, "cli_version": "0.0.0-test", "plan": { "resources.jobs.job": { diff --git a/acceptance/bundle/deploy/readplan/lineage-mismatch/plan.dms.json b/acceptance/bundle/deploy/readplan/lineage-mismatch/plan.dms.json index aa3f25e2d27..7ed9e44220e 100644 --- a/acceptance/bundle/deploy/readplan/lineage-mismatch/plan.dms.json +++ b/acceptance/bundle/deploy/readplan/lineage-mismatch/plan.dms.json @@ -1,5 +1,5 @@ { - "plan_version": 2, + "plan_version": 3, "cli_version": "0.0.0-test", "lineage": "different-lineage", "serial": 0, diff --git a/acceptance/bundle/deploy/readplan/lineage-mismatch/plan.json b/acceptance/bundle/deploy/readplan/lineage-mismatch/plan.json index 42cce15cbfc..80310520edb 100644 --- a/acceptance/bundle/deploy/readplan/lineage-mismatch/plan.json +++ b/acceptance/bundle/deploy/readplan/lineage-mismatch/plan.json @@ -1,5 +1,5 @@ { - "plan_version": 2, + "plan_version": 3, "cli_version": "0.0.0-test", "lineage": "different-lineage", "serial": 0, diff --git a/acceptance/bundle/deploy/readplan/plan-version-mismatch/output.txt b/acceptance/bundle/deploy/readplan/plan-version-mismatch/output.txt index 387aac2335c..3ec4497a23e 100644 --- a/acceptance/bundle/deploy/readplan/plan-version-mismatch/output.txt +++ b/acceptance/bundle/deploy/readplan/plan-version-mismatch/output.txt @@ -1,4 +1,4 @@ -Error: plan version mismatch: plan has version 999 (generated with CLI "0.0.0"), but current version is 2 +Error: plan version mismatch: plan has version 999 (generated with CLI "0.0.0"), but current version is 3 Exit code: 1 diff --git a/acceptance/test.toml b/acceptance/test.toml index 593f9961c60..0e9599e8347 100644 --- a/acceptance/test.toml +++ b/acceptance/test.toml @@ -77,11 +77,11 @@ New = "[METASTORE_NAME]" # Normalize plan_version so bumping it doesn't churn every recorded plan. [[Repls]] -Old = '"plan_version": 2,' +Old = '"plan_version": 3,' New = '"plan_version": [PLAN_VERSION],' [[Repls]] -Old = 'json\.plan_version = 2;' +Old = 'json\.plan_version = 3;' New = 'json.plan_version = [PLAN_VERSION];' # The lineage hash (libs/shortid) is not masked: fixture lineages hash to stable values, and diff --git a/bundle/deployplan/action.go b/bundle/deployplan/action.go index e8f7149aa1a..8e74d4f917e 100644 --- a/bundle/deployplan/action.go +++ b/bundle/deployplan/action.go @@ -43,47 +43,37 @@ type ActionType string // In case of several options, action with highest severity wins. // Note, Create/Delete are handled explicitly and never compared. const ( - Undefined ActionType = "" - Skip ActionType = "skip" - Resize ActionType = "resize" - Update ActionType = "update" - UpdateWithID ActionType = "update_id" - Bind ActionType = "bind" - BindAndUpdate ActionType = "bind_and_update" - Create ActionType = "create" - Recreate ActionType = "recreate" - Delete ActionType = "delete" + Undefined ActionType = "" + Skip ActionType = "skip" + Resize ActionType = "resize" + Update ActionType = "update" + UpdateWithID ActionType = "update_id" + Create ActionType = "create" + Recreate ActionType = "recreate" + Delete ActionType = "delete" ) var actionOrder = map[ActionType]int{ - Undefined: 0, - Skip: 1, - Resize: 2, - Update: 3, - UpdateWithID: 4, - Bind: 5, - BindAndUpdate: 6, - Create: 7, - Recreate: 8, - Delete: 9, + Undefined: 0, + Skip: 1, + Resize: 2, + Update: 3, + UpdateWithID: 4, + Create: 5, + Recreate: 6, + Delete: 7, } // KeepsID reports whether the action preserves an ID already recorded in deployment state. -// Bind actions have a remote ID but no existing state entry. func (a ActionType) KeepsID() bool { switch a { - case Create, UpdateWithID, Recreate, Delete, Bind, BindAndUpdate: + case Create, UpdateWithID, Recreate, Delete: return false default: return true } } -// IsBind returns true if the action is a bind action. -func (a ActionType) IsBind() bool { - return a == Bind || a == BindAndUpdate -} - // StringShort short version of action string, without suffix func (a ActionType) StringShort() string { items := strings.SplitN(string(a), "_", 2) @@ -97,16 +87,8 @@ func (a ActionType) StringShort() string { // so the two are still distinguishable at a glance. func AppliedLine(resourceKey string, action ActionType) string { key := strings.TrimPrefix(resourceKey, "resources.") - // "bind" does not follow the "+d" rule, and bind_and_update names both halves. - switch action { - case Bind: - return "Bound " + key - case BindAndUpdate: - return "Bound and updated " + key - default: - verb := action.StringShort() + "d" - return strings.ToUpper(verb[:1]) + verb[1:] + " " + key - } + verb := action.StringShort() + "d" + return strings.ToUpper(verb[:1]) + verb[1:] + " " + key } // GetHigherAction returns the action with higher severity between a and b. diff --git a/bundle/deployplan/action_test.go b/bundle/deployplan/action_test.go index 2ae3828fa5d..a81d6f34a74 100644 --- a/bundle/deployplan/action_test.go +++ b/bundle/deployplan/action_test.go @@ -9,14 +9,12 @@ import ( func TestAppliedLine(t *testing.T) { for action, expected := range map[deployplan.ActionType]string{ - deployplan.Create: "Created jobs.foo", - deployplan.Update: "Updated jobs.foo", - deployplan.UpdateWithID: "Updated jobs.foo", - deployplan.Resize: "Resized jobs.foo", - deployplan.Recreate: "Recreated jobs.foo", - deployplan.Delete: "Deleted jobs.foo", - deployplan.Bind: "Bound jobs.foo", - deployplan.BindAndUpdate: "Bound and updated jobs.foo", + deployplan.Create: "Created jobs.foo", + deployplan.Update: "Updated jobs.foo", + deployplan.UpdateWithID: "Updated jobs.foo", + deployplan.Resize: "Resized jobs.foo", + deployplan.Recreate: "Recreated jobs.foo", + deployplan.Delete: "Deleted jobs.foo", } { t.Run(string(action), func(t *testing.T) { assert.Equal(t, expected, deployplan.AppliedLine("resources.jobs.foo", action)) diff --git a/bundle/deployplan/plan.go b/bundle/deployplan/plan.go index 1591ac2d770..2f2b7ebf054 100644 --- a/bundle/deployplan/plan.go +++ b/bundle/deployplan/plan.go @@ -18,7 +18,7 @@ import ( "github.com/databricks/cli/libs/structs/structvar" ) -const currentPlanVersion = 2 +const currentPlanVersion = 3 type Plan struct { PlanVersion int `json:"plan_version,omitempty"` @@ -31,6 +31,8 @@ type Plan struct { // deploy --plan rejects a plan whose features differ from the target's (see process.go). Features map[string]struct{} `json:"features,omitempty"` + // Bind commits existing resources to state before the resource actions in Plan. + Bind map[string]*BindEntry `json:"bind,omitempty"` Plan map[string]*PlanEntry `json:"plan,omitzero"` // NotSelected is the number of resources removed by FilterToSelected via the @@ -49,15 +51,13 @@ type ActionCounts struct { Change int Delete int Unchanged int - // Bind counts bind and bind_and_update actions; a bind_and_update also - // counts as a change, since it applies config to the adopted resource. - Bind int + Bind int } // CountActions tallies the plan's actions by category. Order is irrelevant to a // tally, so it iterates the plan map directly rather than the sorted GetActions. func (p *Plan) CountActions() ActionCounts { - var c ActionCounts + c := ActionCounts{Bind: len(p.Bind)} for _, entry := range p.Plan { switch entry.Action { case Create: @@ -77,11 +77,6 @@ func (p *Plan) CountActions() ActionCounts { // A recreate counts as both a delete and a create. c.Delete++ c.Create++ - case Bind: - c.Bind++ - case BindAndUpdate: - c.Bind++ - c.Change++ case Skip, Undefined: c.Unchanged++ } @@ -126,11 +121,17 @@ func LoadPlanFromFile(path string) (*Plan, error) { return &plan, nil } -type PlanEntry struct { - ID string `json:"id,omitempty"` - BindID string `json:"bind_id,omitempty"` +type BindEntry struct { + ID string `json:"id"` + State json.RawMessage `json:"state"` DependsOn []DependsOnEntry `json:"depends_on,omitempty"` - Action ActionType `json:"action,omitempty"` +} + +type PlanEntry struct { + ID string `json:"id,omitempty"` + DependsOn []DependsOnEntry `json:"depends_on,omitempty"` + Action ActionType `json:"action,omitempty"` + PreventIDChange bool `json:"prevent_id_change,omitempty"` // Gone is set on Delete entries when planning confirmed the resource no longer // exists remotely. Applying such an entry only removes it from the state, without // calling the delete API, and approval prompts do not list it as a deletion. @@ -427,6 +428,11 @@ func (p *Plan) FilterToSelected(_ context.Context, selected []string) error { delete(p.Plan, key) } } + for key := range p.Bind { + if _, ok := reachable[key]; !ok { + delete(p.Bind, key) + } + } p.NotSelected = before - len(p.Plan) return nil diff --git a/bundle/deployplan/plan_test.go b/bundle/deployplan/plan_test.go index cee00f42c44..b4b10011f42 100644 --- a/bundle/deployplan/plan_test.go +++ b/bundle/deployplan/plan_test.go @@ -1,6 +1,9 @@ package deployplan_test import ( + "encoding/json" + "os" + "path/filepath" "testing" "github.com/databricks/cli/bundle/deployplan" @@ -9,6 +12,31 @@ import ( "github.com/stretchr/testify/require" ) +func TestSavedPlanPreservesBindPhase(t *testing.T) { + plan := deployplan.NewPlanDirect() + plan.Bind = map[string]*deployplan.BindEntry{ + "resources.jobs.job": {ID: "123", State: json.RawMessage(`{"name":"existing"}`)}, + "resources.experiments.experiment": {ID: "456", State: json.RawMessage(`{"name":"unchanged"}`)}, + } + plan.Plan = map[string]*deployplan.PlanEntry{ + "resources.jobs.job": {ID: "123", Action: deployplan.Update, PreventIDChange: true}, + "resources.experiments.experiment": {ID: "456", Action: deployplan.Skip, PreventIDChange: true}, + } + encoded, err := json.Marshal(plan) + require.NoError(t, err) + path := filepath.Join(t.TempDir(), "plan.json") + require.NoError(t, os.WriteFile(path, encoded, 0o600)) + loaded, err := deployplan.LoadPlanFromFile(path) + require.NoError(t, err) + assert.Equal(t, plan.Bind, loaded.Bind) + assert.Equal(t, plan.Plan, loaded.Plan) + assert.Equal(t, deployplan.ActionCounts{Bind: 2, Change: 1, Unchanged: 1}, loaded.CountActions()) + require.NoError(t, loaded.FilterToSelected(t.Context(), []string{"jobs.job"})) + assert.Len(t, loaded.Bind, 1) + assert.Contains(t, loaded.Bind, "resources.jobs.job") + assert.Equal(t, deployplan.ActionCounts{Bind: 1, Change: 1}, loaded.CountActions()) +} + func TestHasChange(t *testing.T) { tests := []struct { name string diff --git a/bundle/direct/apply.go b/bundle/direct/apply.go index ab90e085278..b4ef97dd5ed 100644 --- a/bundle/direct/apply.go +++ b/bundle/direct/apply.go @@ -36,15 +36,8 @@ func (d *DeploymentUnit) Destroy(ctx context.Context, db *dstate.DeploymentState func (d *DeploymentUnit) Deploy(ctx context.Context, db *dstate.DeploymentState, newState any, actionType deployplan.ActionType, planEntry *deployplan.PlanEntry) error { ctx = log.WithPrefix(ctx, "deploying "+d.ResourceKey) ctx = d.withResourceKey(ctx) - switch actionType { - case deployplan.Create: + if actionType == deployplan.Create { return d.Create(ctx, db, newState) - case deployplan.Bind: - return d.Bind(ctx, db, planEntry.BindID, newState, planEntry) - case deployplan.BindAndUpdate: - return d.BindAndUpdate(ctx, db, planEntry.BindID, newState, planEntry) - default: - // The remaining actions operate on the ID recorded in state. } oldID := db.GetResourceID(d.ResourceKey) @@ -66,49 +59,6 @@ func (d *DeploymentUnit) Deploy(ctx context.Context, db *dstate.DeploymentState, } } -// Bind brings an existing workspace resource under bundle management by persisting -// its ID to state, without modifying the remote resource. -func (d *DeploymentUnit) Bind(ctx context.Context, db *dstate.DeploymentState, id string, newState any, planEntry *deployplan.PlanEntry) error { - // The remote resource was read during planning and is unchanged by a pure bind; - // reuse it so dependents (permissions, grants) do not trigger a second read. - err := d.SetRemoteState(planEntry.RemoteState) - if err != nil { - return err - } - - err = d.saveState(ctx, db, id, newState, d.DependsOn) - if err != nil { - return fmt.Errorf("saving state id=%s: %w", id, err) - } - - log.Infof(ctx, "Bound %s id=%s", d.ResourceKey, id) - return nil -} - -// BindAndUpdate binds an existing workspace resource and applies the configured -// field changes to it. It dispatches on the planned action so the change goes -// through the same path a normal deploy would: Resize for running-cluster -// worker/autoscale drift, Update otherwise. Recreate / UpdateWithID are rejected -// at plan time, so only Update and Resize can reach here. -func (d *DeploymentUnit) BindAndUpdate(ctx context.Context, db *dstate.DeploymentState, id string, newState any, planEntry *deployplan.PlanEntry) error { - action := getMaxAction(planEntry.Changes) - switch action { - case deployplan.Resize: - if err := d.Resize(ctx, db, id, newState, planEntry); err != nil { - return err - } - case deployplan.Update: - if err := d.Update(ctx, db, id, newState, planEntry); err != nil { - return err - } - default: - return fmt.Errorf("internal error: unexpected action %q during bind_and_update", action) - } - - log.Infof(ctx, "Bound and updated %s id=%s", d.ResourceKey, id) - return nil -} - // Create creates the resource and records its state. func (d *DeploymentUnit) Create(ctx context.Context, db *dstate.DeploymentState, newState any) error { var newID string @@ -384,16 +334,24 @@ func (d *DeploymentUnit) Resize(ctx context.Context, db *dstate.DeploymentState, // smaller than a hash placeholder are persisted as is. Sensitive fields are replaced by a // placeholder value so secrets are never written to disk in plaintext. func (d *DeploymentUnit) saveState(ctx context.Context, db *dstate.DeploymentState, newID string, state any, dependsOn []deployplan.DependsOnEntry) error { - compacted, err := dresources.CompactState(d.Adapter.ResourceConfig(), state) + compacted, err := compactStateForSave(d.Adapter, state) if err != nil { - return fmt.Errorf("compacting state: %w", err) - } - if err := zeroSensitiveFields(d.Adapter, compacted); err != nil { - return fmt.Errorf("redacting state: %w", err) + return err } return db.SaveState(ctx, d.ResourceKey, newID, compacted, dependsOn) } +func compactStateForSave(adapter *dresources.Adapter, state any) (any, error) { + compacted, err := dresources.CompactState(adapter.ResourceConfig(), state) + if err != nil { + return nil, fmt.Errorf("compacting state: %w", err) + } + if err := zeroSensitiveFields(adapter, compacted); err != nil { + return nil, fmt.Errorf("redacting state: %w", err) + } + return compacted, nil +} + func parseState(destType reflect.Type, raw json.RawMessage) (any, error) { destPtr := reflect.New(destType).Interface() err := json.Unmarshal(raw, destPtr) diff --git a/bundle/direct/bind_test.go b/bundle/direct/bind_test.go index 982fc883b71..07f6d6a0e3d 100644 --- a/bundle/direct/bind_test.go +++ b/bundle/direct/bind_test.go @@ -10,8 +10,6 @@ import ( "testing" "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/bundle/config/resources" - "github.com/databricks/cli/bundle/deployplan" "github.com/databricks/cli/bundle/direct" "github.com/databricks/cli/bundle/direct/dresources" "github.com/databricks/cli/bundle/direct/dstate" @@ -21,7 +19,7 @@ import ( "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/service/bundledeployments" "github.com/databricks/databricks-sdk-go/service/catalog" - "github.com/databricks/databricks-sdk-go/service/postgres" + "github.com/databricks/databricks-sdk-go/service/dashboards" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -65,13 +63,17 @@ func TestPureBindSanitizesPersistedState(t *testing.T) { if mode != dstate.WritingFile { stateDB.InitializeOperationBuffer(ctx, "123", 1, 1) } - adapter, err := dresources.NewAdapter(dresources.SupportedResources[resourceType], resourceType, client) - require.NoError(t, err) - unit := direct.DeploymentUnit{ - ResourceKey: "resources." + resourceType + ".test", - Adapter: adapter, - } + resourceKey := "resources." + resourceType + ".test" var newState any = &catalog.Secret{Name: "test", Value: secret, EffectiveValue: secret} + server.Handle("GET", "/api/2.1/unity-catalog/secrets/remote-id", func(req testserver.Request) any { + return &catalog.Secret{Name: "test", EffectiveValue: secret} + }) + server.Handle("GET", "/api/2.0/lakeview/dashboards/remote-id", func(req testserver.Request) any { + return &dashboards.Dashboard{DashboardId: "remote-id", SerializedDashboard: content} + }) + server.Handle("GET", "/api/2.0/lakeview/dashboards/remote-id/published", func(req testserver.Request) any { + return &dashboards.PublishedDashboard{} + }) rawContent := secret if resourceType == "dashboards" { newState = &dresources.DashboardState{ @@ -79,8 +81,23 @@ func TestPureBindSanitizesPersistedState(t *testing.T) { } rawContent = content } - require.NoError(t, unit.Bind(ctx, &stateDB, "remote-id", newState, &deployplan.PlanEntry{})) - assert.Equal(t, "remote-id", stateDB.GetResourceID(unit.ResourceKey)) + definitionJSON, err := json.Marshal(newState) + require.NoError(t, err) + var definition map[string]any + require.NoError(t, json.Unmarshal(definitionJSON, &definition)) + delete(definition, "effective_value") + delete(definition, "published") + definition["lifecycle"] = map[string]any{"bind": map[string]string{"id": "remote-id"}} + configJSON, err := json.Marshal(map[string]any{"resources": map[string]any{resourceType: map[string]any{"test": definition}}}) + require.NoError(t, err) + root, diagnostics := config.LoadFromBytes("databricks.yml", configJSON) + require.Empty(t, diagnostics) + var deployment direct.DeploymentBundle + deployment.StateDB.OpenWithData(statePath, dstate.NewDatabase("", 0)) + bindings, err := deployment.PrepareBindings(ctx, client, root, nil) + require.NoError(t, err) + require.NoError(t, stateDB.Bind(ctx, bindings)) + assert.Equal(t, "remote-id", stateDB.GetResourceID(resourceKey)) if mode != dstate.WritingDeploymentHistoryOnly { wal, err := os.ReadFile(statePath + ".wal") require.NoError(t, err) @@ -107,57 +124,3 @@ func TestPureBindSanitizesPersistedState(t *testing.T) { } } } - -func TestBindResolvesStableParentOutputs(t *testing.T) { - for _, parentAction := range []deployplan.ActionType{deployplan.Bind, deployplan.BindAndUpdate} { - for _, managedChild := range []bool{false, true} { - for _, parent := range []string{"projects/p", "${resources.postgres_projects.project.name}"} { - t.Run(fmt.Sprintf("%s/managed=%t/%s", parentAction, managedChild, parent), func(t *testing.T) { - server, client := bindTestClient(t) - server.Handle("GET", "/api/2.0/postgres/projects/p", func(req testserver.Request) any { - return postgres.Project{ - Name: "projects/p", - ProjectId: "p", - Spec: &postgres.ProjectSpec{DisplayName: "existing project"}, - } - }) - server.Handle("GET", "/api/2.0/postgres/projects/p/branches/b", func(req testserver.Request) any { - return postgres.Branch{Name: "projects/p/branches/b", BranchId: "b", Parent: "projects/p"} - }) - source := `{"resources":{"postgres_projects":{"project":{"project_id":"p","display_name":"existing project","lifecycle":{"bind":{"id":"projects/p"}}}},"postgres_branches":{"branch":{"parent":"PARENT","branch_id":"b","lifecycle":{"bind":{"id":"projects/p/branches/b"}}}}}}` - if parentAction == deployplan.BindAndUpdate { - source = strings.ReplaceAll(source, "existing project", "updated project") - } - if managedChild { - source = strings.ReplaceAll(source, `,"lifecycle":{"bind":{"id":"projects/p/branches/b"}}`, "") - } - root, diagnostics := config.LoadFromBytes("databricks.yml", []byte(strings.ReplaceAll(source, "PARENT", parent))) - require.Empty(t, diagnostics) - ctx := logdiag.InitContext(t.Context()) - logdiag.SetCollect(ctx, true) - var deployment direct.DeploymentBundle - statePath := filepath.Join(t.TempDir(), "resources.json") - if managedChild { - require.NoError(t, deployment.StateDB.Open(ctx, statePath, dstate.WritingFile, dstate.WithRecovery(true), dstate.OpenDmsArgs{})) - require.NoError(t, deployment.StateDB.SaveState(ctx, "resources.postgres_branches.branch", "projects/p/branches/b", &resources.PostgresBranchConfig{Parent: "projects/p", BranchId: "b"}, nil)) - _, err := deployment.StateDB.Finalize(ctx) - require.NoError(t, err) - } - require.NoError(t, deployment.StateDB.Open(ctx, statePath, dstate.ReadingFile, dstate.WithRecovery(true), dstate.OpenDmsArgs{})) - t.Cleanup(func() { - _, err := deployment.StateDB.Finalize(ctx) - assert.NoError(t, err) - }) - plan, err := deployment.CalculatePlan(ctx, client, root) - require.NoError(t, err, "%v", logdiag.FlushCollected(ctx)) - assert.Equal(t, parentAction, plan.Plan["resources.postgres_projects.project"].Action) - expected := deployplan.Bind - if managedChild { - expected = deployplan.Skip - } - assert.Equal(t, expected, plan.Plan["resources.postgres_branches.branch"].Action) - }) - } - } - } -} diff --git a/bundle/direct/bundle_apply.go b/bundle/direct/bundle_apply.go index add38bf99f6..34608d4e948 100644 --- a/bundle/direct/bundle_apply.go +++ b/bundle/direct/bundle_apply.go @@ -136,12 +136,21 @@ func (b *DeploymentBundle) Apply(ctx context.Context, client *databricks.Workspa return true } - // We don't keep NewState around for 'skip' nodes - - if action != deployplan.Skip { + // Guarded skips retain NewState so saved plans can recheck the bound resource. + if action != deployplan.Skip || entry.PreventIDChange { if !b.resolveReferences(ctx, resourceKey, entry, errorPrefix, false) { return false } + } + if entry.PreventIDChange { + if err := b.checkBoundResource(ctx, resourceKey, entry, adapter); err != nil { + b.StateDB.RecordFailure(resourceKey, entry.ID, err) + logdiag.LogError(ctx, err) + return false + } + } + + if action != deployplan.Skip { // Get the cached StructVar to check for unresolved refs and get value sv, ok := b.StateCache.Load(resourceKey) diff --git a/bundle/direct/bundle_plan.go b/bundle/direct/bundle_plan.go index 271086fe36e..76fe66c5dd2 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -161,10 +161,7 @@ func (b *DeploymentBundle) StampDeploymentIdForFirstVersion(deploymentID string) // CalculatePlan computes the deployment plan by comparing local config against remote state. // StateDB must already be open for read before calling this function. -// -// Resources with lifecycle.bind that are not yet in state are planned as Bind / -// BindAndUpdate instead of Create; a nil configRoot (destroy) binds nothing. -func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks.WorkspaceClient, configRoot *config.Root) (*deployplan.Plan, error) { +func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks.WorkspaceClient, configRoot *config.Root, selected ...string) (*deployplan.Plan, error) { b.StateDB.AssertOpenedForRead() err := b.init(client) @@ -176,6 +173,11 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks if err != nil { return nil, fmt.Errorf("reading config: %w", err) } + if len(selected) > 0 { + if err := plan.FilterToSelected(ctx, selected); err != nil { + return nil, err + } + } // The plan records the state it was built against so deploy --plan can reject a plan built for a // target of a different shape or a state that has moved on since. @@ -184,9 +186,6 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks b.Plan = plan - // Validate bind blocks against the state before walking the graph; the per-resource - // planning below selects Bind / BindAndUpdate for resources being bound for the - // first time and guards the ones already bound. bindIDByKey := lifecycleBindIDs(configRoot) if len(bindIDByKey) > 0 && !b.validateBindConfig(ctx, configRoot, bindIDByKey) { return nil, errors.New("bind validation failed") @@ -219,6 +218,10 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks } defer plan.WriteUnlockEntry(resourceKey) + _, entry.PreventIDChange = bindIDByKey[resourceKey] + if entry.PreventIDChange { + entry.ID = bindIDByKey[resourceKey] + } if failedDependency != nil { // TODO: this should be a warning @@ -286,12 +289,9 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks // state files may still carry a malformed entry). Treat as missing // and let the resource be re-created on this plan. if !hasEntry || dbentry.ID == "" { - // A resource that is not in state but carries a bind block is imported - // rather than created: read the remote resource under the bound ID and - // plan a Bind / BindAndUpdate instead of a Create. - if bindID, ok := bindIDByKey[resourceKey]; ok { - entry.BindID = bindID - return b.handleBindPlan(ctx, resourceKey, entry, adapter) + if entry.PreventIDChange { + logdiag.LogError(ctx, fmt.Errorf("%s: resource must be bound before deployment planning", errorPrefix)) + return false } entry.Action = deployplan.Create return true @@ -388,12 +388,11 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks return false } - _, bound := bindIDByKey[resourceKey] if remoteState == nil { // A bind block pins the resource to one ID. Creating a replacement would // re-point the state at a new ID behind the user's back and leave the // block mismatched on every later plan, so report the deletion instead. - if bound { + if entry.PreventIDChange { logdiag.LogError(ctx, fmt.Errorf("%s: bound resource with ID %q no longer exists in the workspace; remove lifecycle.bind to create a new resource", errorPrefix, dbentry.ID)) return false } @@ -408,12 +407,8 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks // it can be used for variable resolution depends on several factors, see canReadRemoteCache in LookupReferencePreDeploy b.RemoteStateCache.Store(resourceKey, remoteState) - // An active bind block guards its resource against recreation for as long as - // it is defined, not just during the first bind. Once the resource is in - // state, a config change that would recreate or re-ID it still surfaces as an - // error so the bind block behaves as a persistent guard (see handleBindPlan). - if bound && (action == deployplan.Recreate || action == deployplan.UpdateWithID) { - logdiag.LogError(ctx, buildBindConflictError(resourceKey, action, entry.Changes, false)) + if entry.PreventIDChange && (action == deployplan.Recreate || action == deployplan.UpdateWithID) { + logdiag.LogError(ctx, fmt.Errorf("%s: %w", errorPrefix, buildBindConflictError(action, entry.Changes))) return false } @@ -432,7 +427,7 @@ func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks } for _, entry := range plan.Plan { - if entry.Action == deployplan.Skip { + if entry.Action == deployplan.Skip && !entry.PreventIDChange { entry.NewState = nil } } @@ -1026,11 +1021,6 @@ func (b *DeploymentBundle) LookupReferencePreDeploy(ctx context.Context, path *s } return id, nil } - // A bind adopts the resource under the ID from the bind block; it is known - // before deployment even though the state has no entry yet. - if targetEntry.BindID != "" { - return targetEntry.BindID, nil - } // id may change after deployment, this needs to be done later return nil, errDelayed } @@ -1076,14 +1066,14 @@ func (b *DeploymentBundle) LookupReferencePreDeploy(ctx context.Context, path *s } // A field is safe to read from the remote cache when the target either has no - // changes (Skip or Bind) or retains its remote identity (including BindAndUpdate) + // changes (Skip) or retains its remote identity // and the field cannot have changed: an // immutable field (FieldTriggersRecreate) or a backend-assigned stable output // like an AIP name derived from the ID (FieldIsStableOutput). The latter lets // ${resources.X.name} resolve during an in-place update instead of delaying, // which would otherwise recreate dependents that reference it as their parent. - canReadRemoteCache := targetAction == deployplan.Skip || targetAction == deployplan.Bind || - ((targetAction.KeepsID() || targetAction.IsBind()) && + canReadRemoteCache := targetAction == deployplan.Skip || + (targetAction.KeepsID() && (adapter.FieldTriggersRecreate(fieldPath) || adapter.FieldIsStableOutput(fieldPath))) if configValidErr != nil && remoteValidErr == nil { diff --git a/bundle/direct/bundle_plan_bind.go b/bundle/direct/bundle_plan_bind.go index 9b99b3184b9..b2066bee2bf 100644 --- a/bundle/direct/bundle_plan_bind.go +++ b/bundle/direct/bundle_plan_bind.go @@ -1,10 +1,13 @@ package direct import ( + "bytes" "context" + "encoding/json" "errors" "fmt" "maps" + "reflect" "slices" "strings" @@ -14,8 +17,10 @@ import ( "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/logdiag" "github.com/databricks/cli/libs/retry" + "github.com/databricks/cli/libs/structs/structaccess" "github.com/databricks/cli/libs/structs/structdiff" "github.com/databricks/cli/libs/structs/structpath" + "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/apierr" ) @@ -117,100 +122,250 @@ func typeAndID(resourceKey, bindID string) string { return resourceType + ":" + bindID } -// handleBindPlan reads the remote resource for a resource that is being bound for -// the first time, computes the change set against the local config, and selects -// either Bind (no changes) or BindAndUpdate (config drift can be applied). -// -// Recreate / UpdateWithID actions imply destroying or replacing the workspace -// resource — incompatible with bind — so they surface as actionable errors. -// Returns false if an error was logged. -func (b *DeploymentBundle) handleBindPlan(ctx context.Context, resourceKey string, entry *deployplan.PlanEntry, adapter *dresources.Adapter) bool { - bindID := entry.BindID - errorPrefix := "cannot bind " + resourceKey - - remoteState, err := retry.With(ctx, retry.IsTransientRead, func() (any, error) { - return adapter.DoRead(ctx, bindID) - }) +// PrepareBindings reads and validates resources to adopt without modifying deployment state. +func (b *DeploymentBundle) PrepareBindings(ctx context.Context, client *databricks.WorkspaceClient, configRoot *config.Root, selected []string) (map[string]*deployplan.BindEntry, error) { + b.StateDB.AssertOpenedForRead() + bindIDs := lifecycleBindIDs(configRoot) + if len(bindIDs) == 0 { + return nil, nil + } + if !b.validateBindConfig(ctx, configRoot, bindIDs) { + return nil, errors.New("bind validation failed") + } + if err := b.init(client); err != nil { + return nil, err + } + configured, err := b.makePlan(ctx, configRoot, &b.StateDB.Data) if err != nil { - if apierr.IsMissing(err) { - logdiag.LogError(ctx, fmt.Errorf("%s: resource with ID %q does not exist in workspace", errorPrefix, bindID)) - } else { - logdiag.LogError(ctx, fmt.Errorf("%s: reading remote resource id=%q: %w", errorPrefix, bindID, err)) + return nil, err + } + if len(selected) > 0 { + if err := configured.FilterToSelected(ctx, selected); err != nil { + return nil, err } - return false } - - entry.RemoteState = remoteState - b.RemoteStateCache.Store(resourceKey, remoteState) - - sv, ok := b.StateCache.Load(resourceKey) - if !ok { - logdiag.LogError(ctx, fmt.Errorf("%s: internal error: no state cache entry", errorPrefix)) - return false + bindings := make(map[string]*deployplan.BindEntry) + for _, resourceKey := range slices.Sorted(maps.Keys(bindIDs)) { + entry := configured.Plan[resourceKey] + if entry == nil || b.StateDB.GetResourceID(resourceKey) != "" { + continue + } + adapter, err := b.getAdapterForKey(resourceKey) + if err != nil { + return nil, err + } + remote, err := readBoundResource(ctx, adapter, resourceKey, bindIDs[resourceKey]) + if err != nil { + return nil, err + } + state, err := adapter.RemapState(remote) + if err != nil { + return nil, fmt.Errorf("reading binding state for %s: %w", resourceKey, err) + } + configuredState, _ := b.StateCache.Load(resourceKey) + state, err = prepareBindingState(adapter, state, configuredState.Value) + if err != nil { + return nil, fmt.Errorf("preparing binding for %s: %w", resourceKey, err) + } + encoded, err := json.Marshal(state) + if err != nil { + return nil, fmt.Errorf("serializing binding for %s: %w", resourceKey, err) + } + bindings[resourceKey] = &deployplan.BindEntry{ + ID: bindIDs[resourceKey], State: encoded, DependsOn: entry.DependsOn, + } } + return bindings, nil +} - remoteStateComparable, err := adapter.RemapState(remoteState) +// prepareBindingState imports the remote values of configured fields. Importing +// backend-only fields would claim ownership of defaults the bundle never set; +// importing desired values would hide changes that have not been applied yet. +func prepareBindingState(adapter *dresources.Adapter, remote, configured any) (any, error) { + diff, err := structdiff.GetStructDiff(remote, configured, adapter.KeyedSlices()) if err != nil { - logdiag.LogError(ctx, fmt.Errorf("%s: interpreting remote state: %w", errorPrefix, err)) - return false + return nil, err } - - // Compact both sides on the same fields, so a hashed_fields field is a hash on - // both sides of the diff. sv.Value keeps the full contents for the deploy. - remoteStateComparable, err = dresources.CompactState(adapter.ResourceConfig(), remoteStateComparable) + encoded, err := json.Marshal(remote) if err != nil { - logdiag.LogError(ctx, fmt.Errorf("%s: compacting remote state: %w", errorPrefix, err)) - return false + return nil, err + } + var state any + decoder := json.NewDecoder(bytes.NewReader(encoded)) + decoder.UseNumber() + if err := decoder.Decode(&state); err != nil { + return nil, err + } + // Remove positional array entries from the end so earlier indices stay valid. + for _, change := range slices.Backward(diff) { + value := reflect.ValueOf(change.New) + if value.IsValid() { + switch value.Kind() { + case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Interface: + if !value.IsNil() { + continue + } + default: + continue + } + } + // Dashboards and Genie spaces persist the remote etag for drift detection. + if change.Path.String() == "etag" { + continue + } + if change.Path.IsRoot() { + return nil, errors.New("cannot bind an empty resource configuration") + } + state, err = removeBindingField(state, change.Path.AsSlice()) + if err != nil { + return nil, fmt.Errorf("removing unconfigured field %s: %w", change.Path, err) + } } - localState, err := dresources.CompactState(adapter.ResourceConfig(), sv.Value) + encoded, err = json.Marshal(state) if err != nil { - logdiag.LogError(ctx, fmt.Errorf("%s: compacting local state: %w", errorPrefix, err)) - return false + return nil, err } - - remoteDiff, err := structdiff.GetStructDiff(remoteStateComparable, localState, adapter.KeyedSlices()) + typed, err := parseState(adapter.StateType(), encoded) if err != nil { - logdiag.LogError(ctx, fmt.Errorf("%s: diffing remote state: %w", errorPrefix, err)) - return false + return nil, err } + return compactStateForSave(adapter, typed) +} - // No "saved state" exists for a first-time bind; the diff is purely remote vs. config. - entry.Changes, err = prepareChanges(ctx, adapter, nil, remoteDiff, nil, remoteStateComparable) +// removeBindingField removes a field from a JSON value, including keyed array +// elements such as tasks[task_key='main']. The paths come from structdiff. +func removeBindingField(value any, path []*structpath.PathNode) (any, error) { + node := path[0] + if key, ok := node.StringKey(); ok { + object, ok := value.(map[string]any) + if !ok { + return nil, fmt.Errorf("expected an object at %s", node.Parent()) + } + if len(path) == 1 { + delete(object, key) + return object, nil + } + child, err := removeBindingField(object[key], path[1:]) + if err != nil { + return nil, err + } + object[key] = child + return object, nil + } + sequence, ok := value.([]any) + if !ok { + return nil, fmt.Errorf("expected an array at %s", node.Parent()) + } + index, indexed := node.Index() + if !indexed { + key, match, _ := node.KeyValue() + index = slices.IndexFunc(sequence, func(element any) bool { + field, _ := structaccess.GetByString(element, key) + return field == match + }) + } + if index < 0 || index >= len(sequence) { + return nil, fmt.Errorf("array element %s does not exist", node) + } + if len(path) == 1 { + return slices.Delete(sequence, index, index+1), nil + } + child, err := removeBindingField(sequence[index], path[1:]) if err != nil { - logdiag.LogError(ctx, fmt.Errorf("%s: %w", errorPrefix, err)) - return false + return nil, err } + sequence[index] = child + return sequence, nil +} - err = addPerFieldActions(ctx, adapter, entry.Changes, sv.Value, remoteState) +func readBoundResource(ctx context.Context, adapter *dresources.Adapter, resourceKey, id string) (any, error) { + remote, err := retry.With(ctx, retry.IsTransientRead, func() (any, error) { + return adapter.DoRead(ctx, id) + }) + if apierr.IsMissing(err) || (err == nil && adapter.IsGone(remote)) { + return nil, fmt.Errorf("cannot bind %s: resource with ID %q does not exist in workspace", resourceKey, id) + } if err != nil { - logdiag.LogError(ctx, fmt.Errorf("%s: classifying changes: %w", errorPrefix, err)) - return false + return nil, fmt.Errorf("cannot bind %s: reading remote resource id=%q: %w", resourceKey, id, err) } + return remote, nil +} - switch maxAction := getMaxAction(entry.Changes); maxAction { - case deployplan.Skip, deployplan.Undefined: - entry.Action = deployplan.Bind - case deployplan.Update, deployplan.Resize: - // Same guard as the in-state path: an update can only be applied if the - // resource implements DoUpdate. - if maxAction == deployplan.Update && !adapter.HasDoUpdate() { - logdiag.LogError(ctx, fmt.Errorf("%s: resource does not support update action but plan produced update", errorPrefix)) - return false +// CheckBindings verifies that resources from a saved bind phase still exist. +func (b *DeploymentBundle) CheckBindings(ctx context.Context, client *databricks.WorkspaceClient, bindings map[string]*deployplan.BindEntry) error { + if err := b.init(client); err != nil { + return err + } + for _, key := range slices.Sorted(maps.Keys(bindings)) { + adapter, err := b.getAdapterForKey(key) + if err != nil { + return err + } + if _, err := readBoundResource(ctx, adapter, key, bindings[key].ID); err != nil { + return err } - entry.Action = deployplan.BindAndUpdate - case deployplan.Recreate, deployplan.UpdateWithID: - logdiag.LogError(ctx, buildBindConflictError(resourceKey, maxAction, entry.Changes, true)) - return false - default: - logdiag.LogError(ctx, fmt.Errorf("%s: internal error: unexpected action %q during bind planning", errorPrefix, maxAction)) - return false - } - return true + } + return nil +} + +func (b *DeploymentBundle) checkBoundResource(ctx context.Context, resourceKey string, entry *deployplan.PlanEntry, adapter *dresources.Adapter) error { + if !entry.Action.KeepsID() { + return fmt.Errorf("cannot %s %s: lifecycle.bind requires preserving resource ID %q", entry.Action, resourceKey, entry.ID) + } + id := b.StateDB.GetResourceID(resourceKey) + if id != entry.ID { + return fmt.Errorf("cannot deploy %s: bound resource ID %q does not match state ID %q", resourceKey, entry.ID, id) + } + remote, err := retry.With(ctx, retry.IsTransientRead, func() (any, error) { + return adapter.DoRead(ctx, id) + }) + if apierr.IsMissing(err) || (err == nil && adapter.IsGone(remote)) { + return fmt.Errorf("cannot deploy %s: bound resource with ID %q no longer exists in the workspace; remove lifecycle.bind to create a new resource", resourceKey, id) + } + if err != nil { + return fmt.Errorf("reading bound resource %s: %w", resourceKey, err) + } + configured, ok := b.StateCache.Load(resourceKey) + if !ok { + return fmt.Errorf("cannot deploy %s: missing configured state for bound resource", resourceKey) + } + local, err := dresources.CompactState(adapter.ResourceConfig(), configured.Value) + if err != nil { + return err + } + comparable, err := adapter.RemapState(remote) + if err != nil { + return err + } + comparable, err = dresources.CompactState(adapter.ResourceConfig(), comparable) + if err != nil { + return err + } + diff, err := structdiff.GetStructDiff(comparable, local, adapter.KeyedSlices()) + if err != nil { + return err + } + saved, _ := b.StateDB.GetResourceEntry(resourceKey) + old, err := parseState(adapter.StateType(), saved.State) + if err != nil { + return err + } + changes, err := prepareChanges(ctx, adapter, nil, diff, old, comparable) + if err != nil { + return err + } + if err := addPerFieldActions(ctx, adapter, changes, configured.Value, remote); err != nil { + return err + } + action := getMaxAction(changes) + if action == deployplan.Recreate || action == deployplan.UpdateWithID { + return fmt.Errorf("cannot deploy %s: %w", resourceKey, buildBindConflictError(action, changes)) + } + return nil } -// buildBindConflictError names the incompatible fields and distinguishes creating a -// new resource before adoption from replacing an already-managed resource. -func buildBindConflictError(resourceKey string, action deployplan.ActionType, changes deployplan.Changes, firstBind bool) error { +// buildBindConflictError names the fields that would change the bound resource's ID. +func buildBindConflictError(action deployplan.ActionType, changes deployplan.Changes) error { problematicFields := make([]string, 0, len(changes)) for fieldPath, change := range changes { if change.Action == action { @@ -222,15 +377,11 @@ func buildBindConflictError(resourceKey string, action deployplan.ActionType, ch var headline string // buildBindConflictError is only invoked for Recreate / UpdateWithID; other // action types are guarded by the caller. - switch { //nolint:exhaustive - case action == deployplan.Recreate && firstBind: - headline = "cannot bind " + resourceKey + ": cannot recreate resource with lifecycle.bind" - case action == deployplan.Recreate: - headline = "cannot plan " + resourceKey + ": lifecycle.bind prevents recreating the bound resource" - case action == deployplan.UpdateWithID && firstBind: - headline = "cannot bind " + resourceKey + ": cannot update resource ID with lifecycle.bind" - case action == deployplan.UpdateWithID: - headline = "cannot plan " + resourceKey + ": lifecycle.bind prevents changing the ID of the bound resource" + switch action { //nolint:exhaustive + case deployplan.Recreate: + headline = "lifecycle.bind prevents recreating the bound resource" + case deployplan.UpdateWithID: + headline = "lifecycle.bind prevents changing the ID of the bound resource" } var msg strings.Builder @@ -249,11 +400,7 @@ func buildBindConflictError(resourceKey string, action deployplan.ActionType, ch } } msg.WriteString("\nMatch these fields to the existing resource to preserve its ID") - if firstBind { - msg.WriteString("; remove lifecycle.bind to create a new resource instead of adopting this one") - } else { - msg.WriteString("; remove lifecycle.bind to allow the ID to change") - } + msg.WriteString("; remove lifecycle.bind to allow the ID to change") return errors.New(msg.String()) } diff --git a/bundle/direct/bundle_plan_bind_test.go b/bundle/direct/bundle_plan_bind_test.go new file mode 100644 index 00000000000..344b0370603 --- /dev/null +++ b/bundle/direct/bundle_plan_bind_test.go @@ -0,0 +1,76 @@ +package direct_test + +import ( + "encoding/json" + "path/filepath" + "testing" + + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/direct" + "github.com/databricks/cli/bundle/direct/dstate" + "github.com/databricks/cli/libs/testserver" + "github.com/databricks/databricks-sdk-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPrepareBindingsImportsConfiguredRemoteFields(t *testing.T) { + for _, test := range []struct { + name string + resource string + path string + config string + remote string + state string + }{ + { + name: "immutable backend defaults", + resource: "experiments", + path: "/api/2.0/mlflow/experiments/get", + config: `{"name":"desired"}`, + remote: `{"experiment":{"experiment_id":"123","name":"existing","artifact_location":"dbfs:/existing"}}`, + state: `{"name":"existing"}`, + }, + { + name: "remote etag for drift detection", + resource: "genie_spaces", + path: "/api/2.0/genie/spaces/123", + config: `{"title":"desired"}`, + remote: `{"space_id":"123","title":"existing","etag":"remote-version","parent_path":"/Shared"}`, + state: `{"title":"existing","etag":"remote-version"}`, + }, + { + name: "keyed arrays, maps, explicit zero, and large integers", + resource: "jobs", + path: "/api/2.2/jobs/get", + config: `{"name":"desired","timeout_seconds":0,"tasks":[{"task_key":"a","notebook_task":{"notebook_path":"/a","base_parameters":{"keep":"new"}}},{"task_key":"b","description":"new"}]}`, + remote: `{"job_id":123,"settings":{"name":"existing","timeout_seconds":9007199254740993,"tasks":[{"task_key":"b","description":"old","timeout_seconds":50},{"task_key":"a","notebook_task":{"notebook_path":"/a","base_parameters":{"keep":"old","drop":"value"}}},{"task_key":"unused"}]}}`, + state: `{"name":"existing","timeout_seconds":9007199254740993,"tasks":[{"task_key":"b","description":"old"},{"task_key":"a","notebook_task":{"notebook_path":"/a","base_parameters":{"keep":"old"}}}]}`, + }, + } { + t.Run(test.name, func(t *testing.T) { + server := testserver.New(t) + server.Handle("GET", test.path, func(req testserver.Request) any { + return json.RawMessage(test.remote) + }) + client, err := databricks.NewWorkspaceClient(&databricks.Config{Host: server.URL, Token: "testtoken"}) + require.NoError(t, err) + var resource map[string]any + require.NoError(t, json.Unmarshal([]byte(test.config), &resource)) + resource["lifecycle"] = map[string]any{"bind": map[string]string{"id": "123"}} + source, err := json.Marshal(map[string]any{"resources": map[string]any{test.resource: map[string]any{"test": resource}}}) + require.NoError(t, err) + root, diagnostics := config.LoadFromBytes("databricks.yml", source) + require.Empty(t, diagnostics) + var deployment direct.DeploymentBundle + deployment.StateDB.OpenWithData(filepath.Join(t.TempDir(), "resources.json"), dstate.NewDatabase("", 0)) + bindings, err := deployment.PrepareBindings(t.Context(), client, root, nil) + require.NoError(t, err) + assert.JSONEq(t, test.state, string(bindings["resources."+test.resource+".test"].State)) + if test.resource == "jobs" { + assert.Contains(t, string(bindings["resources.jobs.test"].State), "9007199254740993") + } + assert.Empty(t, deployment.StateDB.Data.State) + }) + } +} diff --git a/bundle/direct/dstate/bind.go b/bundle/direct/dstate/bind.go new file mode 100644 index 00000000000..bd480453023 --- /dev/null +++ b/bundle/direct/dstate/bind.go @@ -0,0 +1,46 @@ +package dstate + +import ( + "context" + "fmt" + "maps" + "slices" + + "github.com/databricks/cli/bundle/deployplan" +) + +// WithBindings returns an independent snapshot for previewing deployment after binding. +func (db *DeploymentState) WithBindings(bindings map[string]*deployplan.BindEntry) Database { + db.AssertOpenedForRead() + db.mu.Lock() + defer db.mu.Unlock() + data := db.Data + data.Features = maps.Clone(data.Features) + data.State = make(map[string]ResourceEntry, len(db.Data.State)+len(bindings)) + maps.Copy(data.State, db.Data.State) + for key, entry := range bindings { + data.State[key] = ResourceEntry{ID: entry.ID, State: entry.State, DependsOn: entry.DependsOn} + } + return data +} + +// Bind records bindings without changing the workspace resources. +func (db *DeploymentState) Bind(ctx context.Context, bindings map[string]*deployplan.BindEntry) error { + db.AssertOpenedForWrite() + keys := slices.Sorted(maps.Keys(bindings)) + for _, key := range keys { + if id := db.GetResourceID(key); id != "" { + return fmt.Errorf("cannot bind %s: already bound to ID %q", key, id) + } + if bindings[key].ID == "" { + return fmt.Errorf("cannot bind %s: missing resource ID", key) + } + } + for _, key := range keys { + entry := bindings[key] + if err := db.SaveState(ctx, key, entry.ID, entry.State, entry.DependsOn); err != nil { + return fmt.Errorf("binding %s: %w", key, err) + } + } + return nil +} diff --git a/bundle/direct/dstate/bind_test.go b/bundle/direct/dstate/bind_test.go new file mode 100644 index 00000000000..78286428137 --- /dev/null +++ b/bundle/direct/dstate/bind_test.go @@ -0,0 +1,50 @@ +package dstate_test + +import ( + "encoding/json" + "path/filepath" + "testing" + + "github.com/databricks/cli/bundle/deployplan" + "github.com/databricks/cli/bundle/direct/dstate" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBindingPreviewDoesNotChangeState(t *testing.T) { + statePath := filepath.Join(t.TempDir(), "resources.json") + var state dstate.DeploymentState + state.OpenWithData(statePath, dstate.NewDatabase("lineage", 4)) + bindings := map[string]*deployplan.BindEntry{ + "resources.jobs.job": {ID: "123", State: json.RawMessage(`{"name":"existing"}`)}, + } + preview := state.WithBindings(bindings) + assert.Equal(t, "123", preview.State["resources.jobs.job"].ID) + assert.Empty(t, state.GetResourceID("resources.jobs.job")) + assert.Empty(t, state.Data.State) + assert.Equal(t, 4, state.GetSerial()) + assert.NoFileExists(t, statePath) + assert.NoFileExists(t, statePath+".wal") +} + +func TestBindRejectsExistingResourcesBeforeWriting(t *testing.T) { + for _, id := range []string{"existing-id", "another-id"} { + t.Run(id, func(t *testing.T) { + statePath := filepath.Join(t.TempDir(), "resources.json") + var state dstate.DeploymentState + data := dstate.NewDatabase("lineage", 1) + data.State["resources.jobs.existing"] = dstate.ResourceEntry{ID: "existing-id", State: json.RawMessage(`{}`)} + state.OpenWithData(statePath, data) + require.NoError(t, state.UpgradeToWrite()) + err := state.Bind(t.Context(), map[string]*deployplan.BindEntry{ + "resources.jobs.a_new": {ID: "new-id", State: json.RawMessage(`{}`)}, + "resources.jobs.existing": {ID: id, State: json.RawMessage(`{}`)}, + }) + require.ErrorContains(t, err, "already bound") + assert.Empty(t, state.GetResourceID("resources.jobs.a_new")) + assert.Equal(t, "existing-id", state.GetResourceID("resources.jobs.existing")) + _, err = state.Finalize(t.Context()) + require.NoError(t, err) + }) + } +} diff --git a/bundle/internal/schema/annotations.yml b/bundle/internal/schema/annotations.yml index a9830a403d0..06fb7490c97 100644 --- a/bundle/internal/schema/annotations.yml +++ b/bundle/internal/schema/annotations.yml @@ -631,7 +631,7 @@ resources: "$fields": "bind": "description": |- - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. "depends_on": "description": |- Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -1122,7 +1122,7 @@ resources: "$fields": "bind": "description": |- - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. "depends_on": "description": |- Resources that must be deployed before this resource and deleted after it. Unlike variable references, this only affects ordering. @@ -1208,7 +1208,7 @@ resources: "$fields": "bind": "description": |- - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. "$fields": "id": "description": |- diff --git a/bundle/phases/declarative_bind.go b/bundle/phases/declarative_bind.go new file mode 100644 index 00000000000..c6c856c01ba --- /dev/null +++ b/bundle/phases/declarative_bind.go @@ -0,0 +1,107 @@ +package phases + +import ( + "context" + "maps" + "slices" + "strings" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/deploy/metadata" + "github.com/databricks/cli/bundle/deployplan" + "github.com/databricks/cli/bundle/direct/dstate" + "github.com/databricks/cli/bundle/statemgmt" + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/dms" + "github.com/databricks/cli/libs/log" + "github.com/databricks/cli/libs/logdiag" + "github.com/databricks/databricks-sdk-go/service/bundledeployments" +) + +func applyBindPhase(ctx context.Context, b *bundle.Bundle, bindings map[string]*deployplan.BindEntry, current *bundledeployments.Deployment) *bundledeployments.Deployment { + if b.MigratingToDirect { + statemgmt.CommitMigration(ctx, b) + if logdiag.HasError(ctx) { + return current + } + b.MigratingToDirect = false + } + db := &b.DeploymentBundle.StateDB + statePath := db.Path + mode := dstate.ReadingFile + if db.IsDeploymentMetadataService() { + mode = dstate.WritingDeploymentHistoryOnly + } + if err := db.UpgradeToWrite(); err != nil { + logdiag.LogError(ctx, err) + return current + } + if db.RecordsHistory() { + var err error + current, err = prepareBindHistory(ctx, b, bindings, current) + if err != nil { + if db.IsDeploymentMetadataService() { + logdiag.LogError(ctx, err) + } else { + log.Warnf(ctx, "Could not record binding history: %s", err) + } + } + } + if !logdiag.HasError(ctx) { + if err := db.Bind(ctx, bindings); err != nil { + logdiag.LogError(ctx, err) + } + } + state, err := db.Finalize(ctx) + b.Metrics.ResourceState = state + if err != nil { + logdiag.LogError(ctx, err) + } + statemgmt.PushResourcesState(ctx, b) + if _, err := db.CompleteVersion(ctx, !logdiag.HasError(ctx)); err != nil { + if db.IsDeploymentMetadataService() { + logdiag.LogError(ctx, err) + } else { + log.Warnf(ctx, "Failed to complete binding history version: %s", err) + } + } + if logdiag.HasError(ctx) { + return current + } + if reportPerResource(b) { + for _, key := range slices.Sorted(maps.Keys(bindings)) { + cmdio.LogString(ctx, "Bound "+strings.TrimPrefix(key, "resources.")) + } + } + history := dstate.OpenDmsArgs{DeploymentID: db.DeploymentID, LastVersionID: db.VersionID} + if err := db.Open(ctx, statePath, mode, dstate.WithRecovery(true), history); err != nil { + logdiag.LogError(ctx, err) + return current + } + if db.RecordsHistory() { + bundle.ApplyContext(ctx, b, metadata.AnnotateDeploymentVersion(db.GetSerial()+1)) + if db.DeploymentID != "" { + bundle.ApplyContext(ctx, b, metadata.AnnotateDeployment(db.DeploymentID)) + } + } + return current +} + +func prepareBindHistory(ctx context.Context, b *bundle.Bundle, bindings map[string]*deployplan.BindEntry, current *bundledeployments.Deployment) (*bundledeployments.Deployment, error) { + current, err := createOrUpdateDeployment(ctx, b, current) + if err != nil { + return current, err + } + staged := make([]bundledeployments.StagedOperation, 0, len(bindings)) + for _, key := range slices.Sorted(maps.Keys(bindings)) { + staged = append(staged, bundledeployments.StagedOperation{ + ResourceKey: strings.TrimPrefix(key, dms.StatePrefix), + ActionType: bundledeployments.OperationActionTypeOperationActionTypeBind, + }) + } + if err := startVersion(ctx, b, dms.VersionTypeDeploy, staged); err != nil { + return current, err + } + logDeploymentVersion(ctx, b) + return current, nil +} diff --git a/bundle/phases/declarative_bind_test.go b/bundle/phases/declarative_bind_test.go new file mode 100644 index 00000000000..5017bc9662a --- /dev/null +++ b/bundle/phases/declarative_bind_test.go @@ -0,0 +1,78 @@ +package phases_test + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/bundle/deployplan" + "github.com/databricks/cli/bundle/direct/dstate" + "github.com/databricks/cli/bundle/phases" + "github.com/databricks/cli/libs/logdiag" + "github.com/databricks/cli/libs/testserver" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/service/postgres" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBindResolvesStableParentOutputs(t *testing.T) { + for _, parentAction := range []deployplan.ActionType{deployplan.Skip, deployplan.Update} { + for _, managedChild := range []bool{false, true} { + for _, parent := range []string{"projects/p", "${resources.postgres_projects.project.name}"} { + t.Run(fmt.Sprintf("%s/managed=%t/%s", parentAction, managedChild, parent), func(t *testing.T) { + server := testserver.New(t) + testserver.AddDefaultHandlers(server) + client, err := databricks.NewWorkspaceClient(&databricks.Config{Host: server.URL, Token: "testtoken"}) + require.NoError(t, err) + server.Handle("GET", "/api/2.0/postgres/projects/p", func(req testserver.Request) any { + return postgres.Project{ + Name: "projects/p", + ProjectId: "p", + Spec: &postgres.ProjectSpec{DisplayName: "existing project"}, + } + }) + server.Handle("GET", "/api/2.0/postgres/projects/p/branches/b", func(req testserver.Request) any { + return postgres.Branch{Name: "projects/p/branches/b", BranchId: "b", Parent: "projects/p"} + }) + source := `{"resources":{"postgres_projects":{"project":{"project_id":"p","display_name":"existing project","lifecycle":{"bind":{"id":"projects/p"}}}},"postgres_branches":{"branch":{"parent":"PARENT","branch_id":"b","lifecycle":{"bind":{"id":"projects/p/branches/b"}}}}}}` + if parentAction == deployplan.Update { + source = strings.ReplaceAll(source, "existing project", "updated project") + } + if managedChild { + source = strings.ReplaceAll(source, `,"lifecycle":{"bind":{"id":"projects/p/branches/b"}}`, "") + } + root, diagnostics := config.LoadFromBytes("databricks.yml", []byte(strings.ReplaceAll(source, "PARENT", parent))) + require.Empty(t, diagnostics) + ctx := logdiag.InitContext(t.Context()) + logdiag.SetCollect(ctx, true) + configured := &bundle.Bundle{Config: *root} + configured.SetWorkpaceClient(client) + deployment := &configured.DeploymentBundle + statePath := filepath.Join(t.TempDir(), "resources.json") + if managedChild { + require.NoError(t, deployment.StateDB.Open(ctx, statePath, dstate.WritingFile, dstate.WithRecovery(true), dstate.OpenDmsArgs{})) + require.NoError(t, deployment.StateDB.SaveState(ctx, "resources.postgres_branches.branch", "projects/p/branches/b", &resources.PostgresBranchConfig{Parent: "projects/p", BranchId: "b"}, nil)) + _, err := deployment.StateDB.Finalize(ctx) + require.NoError(t, err) + } + require.NoError(t, deployment.StateDB.Open(ctx, statePath, dstate.ReadingFile, dstate.WithRecovery(true), dstate.OpenDmsArgs{})) + t.Cleanup(func() { + _, err := deployment.StateDB.Finalize(ctx) + assert.NoError(t, err) + }) + plan := phases.RunPlan(ctx, configured) + require.NotNil(t, plan, "%v", logdiag.FlushCollected(ctx)) + assert.Empty(t, deployment.StateDB.GetResourceID("resources.postgres_projects.project")) + assert.Equal(t, "projects/p", plan.Bind["resources.postgres_projects.project"].ID) + assert.Equal(t, parentAction, plan.Plan["resources.postgres_projects.project"].Action) + assert.Equal(t, deployplan.Skip, plan.Plan["resources.postgres_branches.branch"].Action) + }) + } + } + } +} diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index ef7b2a816ea..40baba56ece 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -15,6 +15,7 @@ import ( "github.com/databricks/cli/bundle/deploy/metadata" "github.com/databricks/cli/bundle/deploy/snapshot" "github.com/databricks/cli/bundle/deployplan" + "github.com/databricks/cli/bundle/direct" "github.com/databricks/cli/bundle/libraries" "github.com/databricks/cli/bundle/metrics" "github.com/databricks/cli/bundle/permissions" @@ -171,7 +172,7 @@ func Deploy(ctx context.Context, b *bundle.Bundle, outputHandler sync.OutputHand // lock is acquired here // - // The version is created only after approval; CompleteVersion is deferred before + // The apply version is created only after approval; CompleteVersion is deferred before // lock.Release and no-ops until then. defer func() { if b.DeploymentBundle.StateDB.RecordsHistory() { @@ -253,10 +254,36 @@ func Deploy(ctx context.Context, b *bundle.Bundle, outputHandler sync.OutputHand } planFromFile := plan != nil + var bindings map[string]*deployplan.BindEntry + if planFromFile { + bindings = plan.Bind + if len(bindings) > 0 { + if err := b.DeploymentBundle.CheckBindings(ctx, b.WorkspaceClient(ctx), bindings); err != nil { + logdiag.LogError(ctx, err) + return + } + } + } else { + var err error + bindings, err = b.DeploymentBundle.PrepareBindings(ctx, b.WorkspaceClient(ctx), &b.Config, b.Select) + if err != nil { + logdiag.LogError(ctx, err) + return + } + } + if len(bindings) > 0 { + dmsDeployment = applyBindPhase(ctx, b, bindings, dmsDeployment) + if logdiag.HasError(ctx) { + return + } + } if plan == nil { // State is already open for read by process.go (or by Migrate) plan = RunPlan(ctx, b) + if plan != nil { + plan.Bind = bindings + } } // Stop before opening the WAL for write if planning failed. UpgradeToWrite @@ -268,9 +295,9 @@ func Deploy(ctx context.Context, b *bundle.Bundle, outputHandler sync.OutputHand haveApproval, err := approvalForDeploy(ctx, b, plan) if !haveApproval { - // No version was created, so the deferred CompleteVersion is a no-op and the version - // number is left for the next deploy. Both the user declining and a console that - // cannot prompt land here. + // No apply version was created, so the deferred CompleteVersion is a no-op and the + // version number is left for the next deploy. Any bind version has already completed. + // Both the user declining and a console that cannot prompt land here. // // A prepared terraform→direct migration lives only in memory (Migrate wrote nothing // durable and CommitMigration below never ran), so a declined deploy just drops it and @@ -358,17 +385,30 @@ func Deploy(ctx context.Context, b *bundle.Bundle, outputHandler sync.OutputHand } func RunPlan(ctx context.Context, b *bundle.Bundle) *deployplan.Plan { - plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config) + bindings, err := b.DeploymentBundle.PrepareBindings(ctx, b.WorkspaceClient(ctx), &b.Config, b.Select) if err != nil { logdiag.LogError(ctx, err) return nil } - if len(b.Select) > 0 { - if err := plan.FilterToSelected(ctx, b.Select); err != nil { - logdiag.LogError(ctx, err) - return nil + deployment := &b.DeploymentBundle + if len(bindings) > 0 { + if deployment.StateDB.RecordsHistory() { + bundle.ApplyContext(ctx, b, metadata.AnnotateDeploymentVersion(deployment.StateDB.GetSerial()+2)) + if logdiag.HasError(ctx) { + return nil + } } + preview := &direct.DeploymentBundle{Adapters: deployment.Adapters} + preview.StateDB.OpenWithData(deployment.StateDB.Path, deployment.StateDB.WithBindings(bindings)) + preview.StateDB.VersionID = deployment.StateDB.VersionID + deployment = preview + } + plan, err := deployment.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config, b.Select...) + if err != nil { + logdiag.LogError(ctx, err) + return nil } + plan.Bind = bindings return plan } diff --git a/bundle/phases/dms.go b/bundle/phases/dms.go index 3b13bb14ee8..5e4a49d8853 100644 --- a/bundle/phases/dms.go +++ b/bundle/phases/dms.go @@ -63,25 +63,21 @@ func actionToSDK(a deployplan.ActionType) (bundledeployments.OperationActionType return bundledeployments.OperationActionTypeOperationActionTypeResize, nil case deployplan.Delete: return bundledeployments.OperationActionTypeOperationActionTypeDelete, nil - case deployplan.Bind: - return bundledeployments.OperationActionTypeOperationActionTypeBind, nil - case deployplan.BindAndUpdate: - return bundledeployments.OperationActionTypeOperationActionTypeBindAndUpdate, nil default: return "", fmt.Errorf("cannot record operation: unsupported action %q", a) } } // createOrUpdateDeployment creates the deployment on a first deploy, or updates the metadata this -// run changed. current is the record the service holds (nil before the first recorded deploy), -// diffed to mask the update down to what changed. Runs after approval, so a declined deploy leaves -// no deployment behind; a first deploy's new id is then stamped into the plan (StampDeploymentID). -func createOrUpdateDeployment(ctx context.Context, b *bundle.Bundle, current *bundledeployments.Deployment) error { +// run changed, returning the current record for later phases. current is the record the service +// holds (nil before the first recorded deploy), diffed to update only changed fields. Binding +// can create the deployment before resource changes are planned and approved. +func createOrUpdateDeployment(ctx context.Context, b *bundle.Bundle, current *bundledeployments.Deployment) (*bundledeployments.Deployment, error) { db := &b.DeploymentBundle w := b.WorkspaceClient(ctx) metadata, err := deploymentMetadata(b) if err != nil { - return fmt.Errorf("failed to compute deployment metadata: %w", err) + return current, fmt.Errorf("failed to compute deployment metadata: %w", err) } deploymentID := db.StateDB.DeploymentID if deploymentID == "" { @@ -89,16 +85,17 @@ func createOrUpdateDeployment(ctx context.Context, b *bundle.Bundle, current *bu dep.InitialParentPath = b.Config.Workspace.StatePath created, err := w.BundleDeployments.CreateDeployment(ctx, bundledeployments.CreateDeploymentRequest{Deployment: dep}) if err != nil { - return fmt.Errorf("failed to create deployment: %w", err) + return current, fmt.Errorf("failed to create deployment: %w", err) } // The id is the workspace node the server makes under the parent path. deploymentID, err = dms.DeploymentIDFromName(created.Name) if err != nil { - return fmt.Errorf("failed to create deployment: %w", err) + return current, fmt.Errorf("failed to create deployment: %w", err) } db.StateDB.DeploymentID = deploymentID + current = created } else if mask := metadata.StaleFields(current); mask != "" { - _, err := retry.With(ctx, retry.IsTransient, func() (*bundledeployments.Deployment, error) { + updated, err := retry.With(ctx, retry.IsTransient, func() (*bundledeployments.Deployment, error) { return w.BundleDeployments.UpdateDeployment(ctx, bundledeployments.UpdateDeploymentRequest{ Name: dms.DeploymentName(deploymentID), Deployment: dms.DeploymentUpdate(metadata, mask), @@ -106,15 +103,16 @@ func createOrUpdateDeployment(ctx context.Context, b *bundle.Bundle, current *bu }) }) if err != nil { - return fmt.Errorf("failed to update deployment: %w", err) + return current, fmt.Errorf("failed to update deployment: %w", err) } + current = updated } // A first deploy had no deployment to read at startup, so its id enters the history here. bundle.ApplyFuncContext(ctx, b, func(_ context.Context, b *bundle.Bundle) { b.Config.Bundle.Deployment.DeploymentID = deploymentID }) - return nil + return current, nil } // deleteDeployment deletes the deployment record. A retry that finds it missing succeeds: the @@ -134,14 +132,11 @@ func deleteDeployment(ctx context.Context, w *databricks.WorkspaceClient, deploy // prepareDeploymentHistory creates history metadata and a version after approval. func prepareDeploymentHistory(ctx context.Context, b *bundle.Bundle, plan *deployplan.Plan, current *bundledeployments.Deployment) error { - firstDeploy := b.DeploymentBundle.StateDB.DeploymentID == "" - if err := createOrUpdateDeployment(ctx, b, current); err != nil { + if _, err := createOrUpdateDeployment(ctx, b, current); err != nil { return err } - if firstDeploy { - if err := b.DeploymentBundle.StampDeploymentIdForFirstVersion(b.DeploymentBundle.StateDB.DeploymentID); err != nil { - return err - } + if err := b.DeploymentBundle.StampDeploymentIdForFirstVersion(b.DeploymentBundle.StateDB.DeploymentID); err != nil { + return err } staged, err := stagedOperations(plan) if err != nil { @@ -160,7 +155,7 @@ func prepareDeploymentHistory(ctx context.Context, b *bundle.Bundle, plan *deplo // prepareDestroyHistory records the approved destroy without changing its state source. func prepareDestroyHistory(ctx context.Context, b *bundle.Bundle, plan *deployplan.Plan) error { if !b.DeploymentBundle.StateDB.IsDeploymentMetadataService() && b.DeploymentBundle.StateDB.DeploymentID == "" { - if err := createOrUpdateDeployment(ctx, b, nil); err != nil { + if _, err := createOrUpdateDeployment(ctx, b, nil); err != nil { return err } } @@ -171,8 +166,8 @@ func prepareDestroyHistory(ctx context.Context, b *bundle.Bundle, plan *deploypl return startVersion(ctx, b, dms.VersionTypeDestroy, staged) } -// startVersion opens a history version after approval. File-backed history skips empty versions; -// DMS-only destroy retains its existing behavior. A declined deploy never claims a version. +// startVersion opens a history version for the phase being committed. File-backed history skips empty versions; +// DMS-only destroy retains its existing behavior. A declined apply never claims a version. // A no-op when the bundle does not record deployment history. func startVersion(ctx context.Context, b *bundle.Bundle, versionType dms.VersionType, staged []bundledeployments.StagedOperation) error { db := &b.DeploymentBundle diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index 819adf6baa3..8043a6db902 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -1697,7 +1697,7 @@ "type": "object", "properties": { "bind": { - "description": "Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption.", + "description": "Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state.", "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.Bind" }, "depends_on": { @@ -1749,7 +1749,7 @@ "type": "object", "properties": { "bind": { - "description": "Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption.", + "description": "Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state.", "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.Bind" }, "depends_on": { @@ -1775,7 +1775,7 @@ "type": "object", "properties": { "bind": { - "description": "Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption.", + "description": "Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state.", "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.Bind" }, "depends_on": { diff --git a/cmd/bundle/plan.go b/cmd/bundle/plan.go index f7294a6e2a9..4ffc2f9bb6a 100644 --- a/cmd/bundle/plan.go +++ b/cmd/bundle/plan.go @@ -3,6 +3,8 @@ package bundle import ( "encoding/json" "fmt" + "maps" + "slices" "strings" "github.com/databricks/cli/bundle" @@ -84,6 +86,18 @@ It is useful for previewing changes before running 'bundle deploy'.`, // Print summary line and actions to stdout totalChanges := counts.Create + counts.Change + counts.Delete + counts.Bind if totalChanges > 0 && bundle.QuietLevel(quiet) < bundle.QuietSummary { + indent := "" + if len(plan.Bind) > 0 { + indent = " " + fmt.Fprintln(out, "Bind:") + for _, key := range slices.Sorted(maps.Keys(plan.Bind)) { + fmt.Fprintf(out, " %s (id: %s)\n", strings.TrimPrefix(key, "resources."), plan.Bind[key].ID) + } + fmt.Fprintln(out, "\nDeploy:") + if counts.Create+counts.Change+counts.Delete == 0 { + fmt.Fprintln(out, " No changes") + } + } // Print all actions in the order they were processed for _, action := range plan.GetActions() { if action.ActionType == deployplan.Skip { @@ -95,14 +109,7 @@ It is useful for previewing changes before running 'bundle deploy'.`, continue } key := strings.TrimPrefix(action.ResourceKey, "resources.") - if action.ActionType.IsBind() { - // Print the full action string (not StringShort) so bind and - // bind_and_update are distinguishable in the text plan. - entry := plan.Plan[action.ResourceKey] - fmt.Fprintf(out, "%s %s (id: %s)\n", action.ActionType, key, entry.BindID) - } else { - fmt.Fprintf(out, "%s %s\n", action.ActionType.StringShort(), key) - } + fmt.Fprintf(out, "%s%s %s\n", indent, action.ActionType.StringShort(), key) } fmt.Fprintln(out) } diff --git a/python/databricks/bundles/alerts/_models/lifecycle.py b/python/databricks/bundles/alerts/_models/lifecycle.py index 3e0828f5e69..f917656cb2c 100644 --- a/python/databricks/bundles/alerts/_models/lifecycle.py +++ b/python/databricks/bundles/alerts/_models/lifecycle.py @@ -19,7 +19,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -45,7 +45,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/apps/_models/lifecycle_with_started.py b/python/databricks/bundles/apps/_models/lifecycle_with_started.py index cefac435c97..e1e244ffae1 100644 --- a/python/databricks/bundles/apps/_models/lifecycle_with_started.py +++ b/python/databricks/bundles/apps/_models/lifecycle_with_started.py @@ -19,7 +19,7 @@ class LifecycleWithStarted: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -50,7 +50,7 @@ class LifecycleWithStartedDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/catalogs/_models/lifecycle.py b/python/databricks/bundles/catalogs/_models/lifecycle.py index 107e93f2a48..5656ce99205 100644 --- a/python/databricks/bundles/catalogs/_models/lifecycle.py +++ b/python/databricks/bundles/catalogs/_models/lifecycle.py @@ -19,7 +19,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -45,7 +45,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/cluster_policies/_models/lifecycle.py b/python/databricks/bundles/cluster_policies/_models/lifecycle.py index e38185faedf..0563fe47d11 100644 --- a/python/databricks/bundles/cluster_policies/_models/lifecycle.py +++ b/python/databricks/bundles/cluster_policies/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/clusters/_models/lifecycle_with_started.py b/python/databricks/bundles/clusters/_models/lifecycle_with_started.py index 7a932c06d88..6c7dbdeda79 100644 --- a/python/databricks/bundles/clusters/_models/lifecycle_with_started.py +++ b/python/databricks/bundles/clusters/_models/lifecycle_with_started.py @@ -19,7 +19,7 @@ class LifecycleWithStarted: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -50,7 +50,7 @@ class LifecycleWithStartedDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/dashboards/_models/lifecycle.py b/python/databricks/bundles/dashboards/_models/lifecycle.py index 045fe6f16c7..d0d69eaa435 100644 --- a/python/databricks/bundles/dashboards/_models/lifecycle.py +++ b/python/databricks/bundles/dashboards/_models/lifecycle.py @@ -19,7 +19,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -45,7 +45,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/data_quality_monitors/_models/lifecycle.py b/python/databricks/bundles/data_quality_monitors/_models/lifecycle.py index 8aada7d26ae..baba7fc3b85 100644 --- a/python/databricks/bundles/data_quality_monitors/_models/lifecycle.py +++ b/python/databricks/bundles/data_quality_monitors/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/database_catalogs/_models/lifecycle.py b/python/databricks/bundles/database_catalogs/_models/lifecycle.py index 021237f4da7..928edc9c745 100644 --- a/python/databricks/bundles/database_catalogs/_models/lifecycle.py +++ b/python/databricks/bundles/database_catalogs/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/database_instances/_models/lifecycle.py b/python/databricks/bundles/database_instances/_models/lifecycle.py index f74e28445d1..7cf4ede5e49 100644 --- a/python/databricks/bundles/database_instances/_models/lifecycle.py +++ b/python/databricks/bundles/database_instances/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/experiments/_models/lifecycle.py b/python/databricks/bundles/experiments/_models/lifecycle.py index c70097e927b..332aec97859 100644 --- a/python/databricks/bundles/experiments/_models/lifecycle.py +++ b/python/databricks/bundles/experiments/_models/lifecycle.py @@ -19,7 +19,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -45,7 +45,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/external_locations/_models/lifecycle.py b/python/databricks/bundles/external_locations/_models/lifecycle.py index 2b82a5ca4ac..186622e0a4d 100644 --- a/python/databricks/bundles/external_locations/_models/lifecycle.py +++ b/python/databricks/bundles/external_locations/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/features/_models/lifecycle.py b/python/databricks/bundles/features/_models/lifecycle.py index 088937d751a..982d9ace1a8 100644 --- a/python/databricks/bundles/features/_models/lifecycle.py +++ b/python/databricks/bundles/features/_models/lifecycle.py @@ -19,7 +19,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -45,7 +45,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/genie_spaces/_models/lifecycle.py b/python/databricks/bundles/genie_spaces/_models/lifecycle.py index 8fb76376854..6ff1b5a31a3 100644 --- a/python/databricks/bundles/genie_spaces/_models/lifecycle.py +++ b/python/databricks/bundles/genie_spaces/_models/lifecycle.py @@ -19,7 +19,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -45,7 +45,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/instance_pools/_models/lifecycle.py b/python/databricks/bundles/instance_pools/_models/lifecycle.py index 7378572c2a3..1f3d0c9bdb3 100644 --- a/python/databricks/bundles/instance_pools/_models/lifecycle.py +++ b/python/databricks/bundles/instance_pools/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/job_runs/_models/job_run_lifecycle.py b/python/databricks/bundles/job_runs/_models/job_run_lifecycle.py index 7a012daddd5..0826d659b95 100644 --- a/python/databricks/bundles/job_runs/_models/job_run_lifecycle.py +++ b/python/databricks/bundles/job_runs/_models/job_run_lifecycle.py @@ -23,7 +23,7 @@ class JobRunLifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -54,7 +54,7 @@ class JobRunLifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/jobs/_models/lifecycle.py b/python/databricks/bundles/jobs/_models/lifecycle.py index 6124d357626..d4e32783c29 100644 --- a/python/databricks/bundles/jobs/_models/lifecycle.py +++ b/python/databricks/bundles/jobs/_models/lifecycle.py @@ -19,7 +19,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -45,7 +45,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/materialization_sets/_models/lifecycle.py b/python/databricks/bundles/materialization_sets/_models/lifecycle.py index 710a5ac3ccf..f1c12a2b86a 100644 --- a/python/databricks/bundles/materialization_sets/_models/lifecycle.py +++ b/python/databricks/bundles/materialization_sets/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/mcp_services/_models/lifecycle.py b/python/databricks/bundles/mcp_services/_models/lifecycle.py index 49ede7eecd0..981e3111f60 100644 --- a/python/databricks/bundles/mcp_services/_models/lifecycle.py +++ b/python/databricks/bundles/mcp_services/_models/lifecycle.py @@ -19,7 +19,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -45,7 +45,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/model_provider_services/_models/lifecycle.py b/python/databricks/bundles/model_provider_services/_models/lifecycle.py index b8ac891a71c..e48cefd7dfc 100644 --- a/python/databricks/bundles/model_provider_services/_models/lifecycle.py +++ b/python/databricks/bundles/model_provider_services/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/model_services/_models/lifecycle.py b/python/databricks/bundles/model_services/_models/lifecycle.py index 55690e15c89..475cbfa0b36 100644 --- a/python/databricks/bundles/model_services/_models/lifecycle.py +++ b/python/databricks/bundles/model_services/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/model_serving_endpoints/_models/lifecycle.py b/python/databricks/bundles/model_serving_endpoints/_models/lifecycle.py index 4690fd5eb02..61e5b85fd69 100644 --- a/python/databricks/bundles/model_serving_endpoints/_models/lifecycle.py +++ b/python/databricks/bundles/model_serving_endpoints/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/models/_models/lifecycle.py b/python/databricks/bundles/models/_models/lifecycle.py index ae1b6916991..ca34d77ab2e 100644 --- a/python/databricks/bundles/models/_models/lifecycle.py +++ b/python/databricks/bundles/models/_models/lifecycle.py @@ -19,7 +19,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -45,7 +45,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/pipelines/_models/lifecycle.py b/python/databricks/bundles/pipelines/_models/lifecycle.py index cdbc755571f..309c83efe3a 100644 --- a/python/databricks/bundles/pipelines/_models/lifecycle.py +++ b/python/databricks/bundles/pipelines/_models/lifecycle.py @@ -19,7 +19,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -45,7 +45,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/quality_monitors/_models/lifecycle.py b/python/databricks/bundles/quality_monitors/_models/lifecycle.py index e7dafdb464e..d7305fdcc1c 100644 --- a/python/databricks/bundles/quality_monitors/_models/lifecycle.py +++ b/python/databricks/bundles/quality_monitors/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/registered_models/_models/lifecycle.py b/python/databricks/bundles/registered_models/_models/lifecycle.py index 9a42fa739e1..9200d60f957 100644 --- a/python/databricks/bundles/registered_models/_models/lifecycle.py +++ b/python/databricks/bundles/registered_models/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/schemas/_models/lifecycle.py b/python/databricks/bundles/schemas/_models/lifecycle.py index d1fb0845e76..92cff06c17e 100644 --- a/python/databricks/bundles/schemas/_models/lifecycle.py +++ b/python/databricks/bundles/schemas/_models/lifecycle.py @@ -19,7 +19,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -45,7 +45,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/secret_scopes/_models/lifecycle.py b/python/databricks/bundles/secret_scopes/_models/lifecycle.py index 0e7190c3414..bbda9f5aa4d 100644 --- a/python/databricks/bundles/secret_scopes/_models/lifecycle.py +++ b/python/databricks/bundles/secret_scopes/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/secrets/_models/lifecycle.py b/python/databricks/bundles/secrets/_models/lifecycle.py index 25247266ed5..5b9d137c32b 100644 --- a/python/databricks/bundles/secrets/_models/lifecycle.py +++ b/python/databricks/bundles/secrets/_models/lifecycle.py @@ -19,7 +19,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -45,7 +45,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/sql_warehouses/_models/lifecycle_with_started.py b/python/databricks/bundles/sql_warehouses/_models/lifecycle_with_started.py index c62c481c96b..eadf7002c22 100644 --- a/python/databricks/bundles/sql_warehouses/_models/lifecycle_with_started.py +++ b/python/databricks/bundles/sql_warehouses/_models/lifecycle_with_started.py @@ -22,7 +22,7 @@ class LifecycleWithStarted: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -53,7 +53,7 @@ class LifecycleWithStartedDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/synced_database_tables/_models/lifecycle.py b/python/databricks/bundles/synced_database_tables/_models/lifecycle.py index b61b74a526f..4fe2e4da418 100644 --- a/python/databricks/bundles/synced_database_tables/_models/lifecycle.py +++ b/python/databricks/bundles/synced_database_tables/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/vector_search_endpoints/_models/lifecycle.py b/python/databricks/bundles/vector_search_endpoints/_models/lifecycle.py index 2f54e82b59a..ff32955adcb 100644 --- a/python/databricks/bundles/vector_search_endpoints/_models/lifecycle.py +++ b/python/databricks/bundles/vector_search_endpoints/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/vector_search_indexes/_models/lifecycle.py b/python/databricks/bundles/vector_search_indexes/_models/lifecycle.py index c4047afd6e4..38d49d095d3 100644 --- a/python/databricks/bundles/vector_search_indexes/_models/lifecycle.py +++ b/python/databricks/bundles/vector_search_indexes/_models/lifecycle.py @@ -22,7 +22,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -48,7 +48,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam] diff --git a/python/databricks/bundles/volumes/_models/lifecycle.py b/python/databricks/bundles/volumes/_models/lifecycle.py index c7013db6ce7..281e5b05ac8 100644 --- a/python/databricks/bundles/volumes/_models/lifecycle.py +++ b/python/databricks/bundles/volumes/_models/lifecycle.py @@ -19,7 +19,7 @@ class Lifecycle: bind: VariableOrOptional[Bind] = None """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOn] = field(default_factory=list) @@ -45,7 +45,7 @@ class LifecycleDict(TypedDict, total=False): bind: VariableOrOptional[BindParam] """ - Existing workspace resource to adopt into the deployment state instead of creating a new one. The first deploy binds it (`bind`) and applies the configuration if it differs (`bind_and_update`), including target settings such as `mode: development`. While the block is present the resource cannot be recreated; removing the resource or the block ends that protection, and `bundle destroy` deletes bound resources like any other. Nothing is recorded in the state about the adoption. + Existing workspace resource to adopt before deployment. Deploy first records its ID and configuration in remote state, then applies normal updates, including target settings such as `mode: development`. Binding remains recorded if deployment fails. While present, this block prevents creation, recreation, and ID changes; removing it does not unbind the resource. `bundle destroy` still deletes bound resources. No bind marker is stored in resource state. """ depends_on: VariableOrList[DependsOnParam]