From d5c2ca3b68aaa34326def272cd1a462a8b9ee8f3 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Thu, 7 May 2026 12:40:21 +0200 Subject: [PATCH 01/24] Add declarative bind for direct deployment engine Allow users to bring existing workspace resources under bundle management declaratively via `bind` blocks under a target. Adds: - `Bind` config type and `bind` field on `Target` in `bundle/config`, with a structural validator that rejects child resources (`*.permissions`, `*.grants`). - `Bind` and `BindAndUpdate` action types in the deploy plan, surfaced in `bundle plan` output (and in the summary as "N to bind"). - `DeploymentBundle.ApplyBindToPlan` (in `bundle/direct/bundle_plan_bind.go`), which runs after `CalculatePlan`, validates bind references, detects bind-ID/state-ID conflicts, and reads remote state to choose `Bind` vs `BindAndUpdate` per resource. - `DeploymentUnit.DeclarativeBind` apply path that persists the bound ID (and optionally invokes `DoUpdate` for `BindAndUpdate`). - Targeted error in `libs/dyn/convert/normalize.go` for `bind` placed at the root level. - Rejection of bind blocks in the terraform path (`RunPlan`), `bundle deployment migrate`, and `bundle destroy` when the bind matches a resource currently in state. Acceptance coverage in `acceptance/bundle/deploy/bind/` covers the happy path (basic, bind_and_update), validation errors (invalid resource type, orphaned bind, top-level bind, duplicate id, terraform engine, child resources), runtime errors (resource not found, recreate blocked), and the destroy / migrate guards. Co-Authored-By: Claude Opus 4.7 Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 + .../bundle/deploy/bind/basic/databricks.yml | 23 ++ acceptance/bundle/deploy/bind/basic/hello.py | 1 + .../bundle/deploy/bind/basic/out.test.toml | 3 + .../bundle/deploy/bind/basic/output.txt | 66 +++++ acceptance/bundle/deploy/bind/basic/script | 37 +++ .../bind/bind-and-update/databricks.yml | 23 ++ .../deploy/bind/bind-and-update/hello.py | 1 + .../deploy/bind/bind-and-update/out.test.toml | 3 + .../deploy/bind/bind-and-update/output.txt | 37 +++ .../bundle/deploy/bind/bind-and-update/script | 44 +++ .../bind/bind-permissions/databricks.yml | 21 ++ .../bundle/deploy/bind/bind-permissions/nb.py | 2 + .../bind/bind-permissions/out.test.toml | 3 + .../deploy/bind/bind-permissions/output.txt | 25 ++ .../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 | 3 + .../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 | 3 + .../bind/delete-and-bind-conflict/output.txt | 22 ++ .../bind/delete-and-bind-conflict/script | 18 ++ .../bind/delete-and-bind-conflict/test.toml | 1 + .../bind/destroy-blocked/databricks.yml | 23 ++ .../deploy/bind/destroy-blocked/hello.py | 1 + .../deploy/bind/destroy-blocked/out.test.toml | 3 + .../deploy/bind/destroy-blocked/output.txt | 19 ++ .../bundle/deploy/bind/destroy-blocked/script | 30 ++ .../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 | 3 + .../deploy/bind/duplicate-bind-id/output.txt | 22 ++ .../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 | 3 + .../bind/invalid-resource-type/output.txt | 7 + .../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 | 3 + .../deploy/bind/orphaned-bind/output.txt | 7 + .../bundle/deploy/bind/orphaned-bind/script | 2 + .../bind/recreate-blocked/databricks.yml | 18 ++ .../deploy/bind/recreate-blocked/nb.sql | 2 + .../bind/recreate-blocked/out.test.toml | 3 + .../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 | 3 + .../deploy/bind/resource-not-found/output.txt | 6 + .../deploy/bind/resource-not-found/script | 2 + .../bind/terraform-with-bind/databricks.yml | 23 ++ .../deploy/bind/terraform-with-bind/hello.py | 1 + .../bind/terraform-with-bind/out.test.toml | 3 + .../bind/terraform-with-bind/output.txt | 4 + .../deploy/bind/terraform-with-bind/script | 2 + .../deploy/bind/terraform-with-bind/test.toml | 3 + acceptance/bundle/deploy/bind/test.toml | 5 + .../deploy/bind/top-level-bind/databricks.yml | 21 ++ .../deploy/bind/top-level-bind/hello.py | 1 + .../deploy/bind/top-level-bind/out.test.toml | 3 + .../deploy/bind/top-level-bind/output.txt | 9 + .../bundle/deploy/bind/top-level-bind/script | 1 + .../deploy/bind/top-level-bind/test.toml | 1 + bundle/config/bind.go | 86 ++++++ 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 | 43 +-- bundle/deployplan/plan.go | 1 + bundle/direct/apply.go | 32 +++ bundle/direct/bundle_apply.go | 2 + bundle/direct/bundle_plan_bind.go | 263 ++++++++++++++++++ bundle/internal/schema/annotations.yml | 7 + bundle/phases/deploy.go | 13 + bundle/phases/destroy.go | 35 +++ bundle/phases/initialize.go | 4 + bundle/schema/jsonschema.json | 63 +++++ cmd/bundle/deployment/migrate.go | 8 + cmd/bundle/plan.go | 21 +- libs/dyn/convert/normalize.go | 15 + libs/dyn/convert/normalize_test.go | 19 ++ 93 files changed, 1571 insertions(+), 20 deletions(-) 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-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-blocked/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/destroy-blocked/hello.py create mode 100644 acceptance/bundle/deploy/bind/destroy-blocked/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/destroy-blocked/output.txt create mode 100644 acceptance/bundle/deploy/bind/destroy-blocked/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/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/terraform-with-bind/databricks.yml create mode 100644 acceptance/bundle/deploy/bind/terraform-with-bind/hello.py create mode 100644 acceptance/bundle/deploy/bind/terraform-with-bind/out.test.toml create mode 100644 acceptance/bundle/deploy/bind/terraform-with-bind/output.txt create mode 100644 acceptance/bundle/deploy/bind/terraform-with-bind/script create mode 100644 acceptance/bundle/deploy/bind/terraform-with-bind/test.toml 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/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 00152d550ea..b7982c5a568 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -5,5 +5,6 @@ ### CLI ### Bundles +* 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)). ### Dependency updates 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..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/basic/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/deploy/bind/basic/output.txt b/acceptance/bundle/deploy/bind/basic/output.txt new file mode 100644 index 00000000000..74586828ae0 --- /dev/null +++ b/acceptance/bundle/deploy/bind/basic/output.txt @@ -0,0 +1,66 @@ + +>>> [CLI] bundle plan +bind 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... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged + +>>> print_state.py +{ + "state_version": 2, + "cli_version": "[DEV_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 + +Deleting files... +Destroy complete! diff --git a/acceptance/bundle/deploy/bind/basic/script b/acceptance/bundle/deploy/bind/basic/script new file mode 100644 index 00000000000..29ed7d5e6f9 --- /dev/null +++ b/acceptance/bundle/deploy/bind/basic/script @@ -0,0 +1,37 @@ +# 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" + +# Remove bind block before destroy (destroy blocks on active bind blocks) +python3 << 'PYSCRIPT' +import re +with open('databricks.yml', 'r') as f: + content = f.read() + +# Remove the bind block from the target (everything from "bind:" until the next top-level key or EOF) +content = re.sub(r'\n bind:.*', '', content, flags=re.DOTALL) + +with open('databricks.yml', 'w') as f: + f.write(content) +PYSCRIPT + +# Remove .databricks directory that might cache old config +rm -rf .databricks + +# Cleanup +trace $CLI bundle destroy --auto-approve 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..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-update/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] 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..92b414476c2 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-update/output.txt @@ -0,0 +1,37 @@ + +>>> [CLI] bundle plan +bind 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... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [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... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [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 + +Deleting files... +Destroy complete! 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..080427e7598 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-update/script @@ -0,0 +1,44 @@ +# 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 + +# Remove bind block before destroy (destroy blocks on active bind blocks) +python3 << 'PYSCRIPT' +import re +with open('databricks.yml', 'r') as f: + content = f.read() + +# Remove the bind block from the target (everything from "bind:" until the next top-level key or EOF) +content = re.sub(r'\n bind:.*', '', content, flags=re.DOTALL) + +with open('databricks.yml', 'w') as f: + f.write(content) +PYSCRIPT + +# Remove .databricks directory that might cache old config +rm -rf .databricks + +# Cleanup +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..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-permissions/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] 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..3510df058e9 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-permissions/output.txt @@ -0,0 +1,25 @@ + +>>> musterr [CLI] bundle validate +Error: binding jobs.permissions is not allowed + +bind can only be used for resources directly under the resources block, not for child resources like permissions or grants. + +To manage permissions or grants: +1. First bind the parent resource (without .permissions or .grants) +2. Then define permissions or grants in your bundle configuration + +Invalid bind configuration: + bind: + jobs.permissions: + foo: + id: ... + +Instead, remove this bind entry and ensure the parent resource is bound. + +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..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/block-migrate/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] 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..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] 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..57e63fae484 --- /dev/null +++ b/acceptance/bundle/deploy/bind/delete-and-bind-conflict/output.txt @@ -0,0 +1,22 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-delete-conflict/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> 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 + +Deleting files... +Destroy complete! 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-blocked/databricks.yml b/acceptance/bundle/deploy/bind/destroy-blocked/databricks.yml new file mode 100644 index 00000000000..f60645ff51f --- /dev/null +++ b/acceptance/bundle/deploy/bind/destroy-blocked/databricks.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-destroy-blocked + +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-blocked/hello.py b/acceptance/bundle/deploy/bind/destroy-blocked/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/deploy/bind/destroy-blocked/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/deploy/bind/destroy-blocked/out.test.toml b/acceptance/bundle/deploy/bind/destroy-blocked/out.test.toml new file mode 100644 index 00000000000..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/destroy-blocked/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/deploy/bind/destroy-blocked/output.txt b/acceptance/bundle/deploy/bind/destroy-blocked/output.txt new file mode 100644 index 00000000000..9f8ca5a5c60 --- /dev/null +++ b/acceptance/bundle/deploy/bind/destroy-blocked/output.txt @@ -0,0 +1,19 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-destroy-blocked/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> musterr [CLI] bundle destroy --auto-approve +Error: cannot destroy with bind blocks that reference resources in the deployment state: resources.jobs.foo; remove the bind blocks from the target configuration or run 'bundle deployment unbind' before destroying + + +>>> [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-blocked/default + +Deleting files... +Destroy complete! diff --git a/acceptance/bundle/deploy/bind/destroy-blocked/script b/acceptance/bundle/deploy/bind/destroy-blocked/script new file mode 100644 index 00000000000..508cbc0a043 --- /dev/null +++ b/acceptance/bundle/deploy/bind/destroy-blocked/script @@ -0,0 +1,30 @@ +# 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 + +# Try to destroy - should fail because bind blocks reference resources in state +trace musterr $CLI bundle destroy --auto-approve + +# Remove bind block and destroy to clean up +python3 << 'PYSCRIPT' +import re +with open('databricks.yml', 'r') as f: + content = f.read() + +# Remove the bind block from the target (everything from "bind:" until the next top-level key or EOF) +content = re.sub(r'\n bind:.*', '', content, flags=re.DOTALL) + +with open('databricks.yml', 'w') as f: + f.write(content) +PYSCRIPT + +# Remove .databricks directory that might cache old config +rm -rf .databricks + +trace $CLI bundle destroy --auto-approve 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..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] 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..60a188806ba --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id/output.txt @@ -0,0 +1,22 @@ + +>>> [CLI] bundle deploy --auto-approve +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-duplicate-id/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> 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 + +Deleting files... +Destroy complete! 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..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/invalid-resource-type/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] 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..d06b74c8f14 --- /dev/null +++ b/acceptance/bundle/deploy/bind/invalid-resource-type/output.txt @@ -0,0 +1,7 @@ + +>>> musterr [CLI] bundle plan +Error: bind block references undefined resource "resources.foobar.foo"; define it in the resources section or remove the bind block + at targets.default.bind.foobar.foo + +Error: bind validation failed + 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..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/orphaned-bind/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] 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/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..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recreate-blocked/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] 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..5ddf2b0be75 --- /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: bind 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..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/resource-not-found/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] 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..96ef84cc0a8 --- /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 "[NUMID]" does not exist in workspace + +Error: bind 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/terraform-with-bind/databricks.yml b/acceptance/bundle/deploy/bind/terraform-with-bind/databricks.yml new file mode 100644 index 00000000000..98450928340 --- /dev/null +++ b/acceptance/bundle/deploy/bind/terraform-with-bind/databricks.yml @@ -0,0 +1,23 @@ +bundle: + name: test-bind-terraform + +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/terraform-with-bind/hello.py b/acceptance/bundle/deploy/bind/terraform-with-bind/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/bundle/deploy/bind/terraform-with-bind/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/bundle/deploy/bind/terraform-with-bind/out.test.toml b/acceptance/bundle/deploy/bind/terraform-with-bind/out.test.toml new file mode 100644 index 00000000000..42c0997090a --- /dev/null +++ b/acceptance/bundle/deploy/bind/terraform-with-bind/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] diff --git a/acceptance/bundle/deploy/bind/terraform-with-bind/output.txt b/acceptance/bundle/deploy/bind/terraform-with-bind/output.txt new file mode 100644 index 00000000000..62c0d2b6c5f --- /dev/null +++ b/acceptance/bundle/deploy/bind/terraform-with-bind/output.txt @@ -0,0 +1,4 @@ + +>>> musterr [CLI] bundle plan +Error: bind blocks in the target configuration are only supported with the direct deployment engine; set DATABRICKS_BUNDLE_ENGINE=direct or remove the bind blocks + diff --git a/acceptance/bundle/deploy/bind/terraform-with-bind/script b/acceptance/bundle/deploy/bind/terraform-with-bind/script new file mode 100644 index 00000000000..4b3c7a8ecde --- /dev/null +++ b/acceptance/bundle/deploy/bind/terraform-with-bind/script @@ -0,0 +1,2 @@ +# Import blocks should error with terraform engine +trace musterr $CLI bundle plan diff --git a/acceptance/bundle/deploy/bind/terraform-with-bind/test.toml b/acceptance/bundle/deploy/bind/terraform-with-bind/test.toml new file mode 100644 index 00000000000..272dde4b9ce --- /dev/null +++ b/acceptance/bundle/deploy/bind/terraform-with-bind/test.toml @@ -0,0 +1,3 @@ +# Override engine to terraform to test import block rejection +[EnvMatrix] +DATABRICKS_BUNDLE_ENGINE = ["terraform"] diff --git a/acceptance/bundle/deploy/bind/test.toml b/acceptance/bundle/deploy/bind/test.toml new file mode 100644 index 00000000000..931833f6cc5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/test.toml @@ -0,0 +1,5 @@ +Cloud = true +Ignore = [".databricks"] + +[EnvMatrix] +DATABRICKS_BUNDLE_ENGINE = ["direct"] 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..e90b6d5d1ba --- /dev/null +++ b/acceptance/bundle/deploy/bind/top-level-bind/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] 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..9425c8feb52 --- /dev/null +++ b/acceptance/bundle/deploy/bind/top-level-bind/output.txt @@ -0,0 +1,9 @@ + +>>> musterr [CLI] bundle validate +Error: bind blocks are not allowed at the root level + in databricks.yml:4:1 + +bind blocks must be defined within a target. Move the bind configuration under targets..bind + + +Found 1 error 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..44dbe24759c --- /dev/null +++ b/acceptance/bundle/deploy/bind/top-level-bind/script @@ -0,0 +1 @@ +trace musterr $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/bundle/config/bind.go b/bundle/config/bind.go new file mode 100644 index 00000000000..943f3d9582a --- /dev/null +++ b/bundle/config/bind.go @@ -0,0 +1,86 @@ +package config + +import ( + "fmt" + "slices" + "strings" + + "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 that target child resources (e.g. "jobs.permissions"). +// The direct engine exposes child resources as full keys like "jobs.permissions" in its +// resource registry, but they are not bindable on their own — bind the parent resource +// and let the bundle manage the child entries declaratively. +func (b Bind) Validate() diag.Diagnostics { + var diags diag.Diagnostics + + for resourceType, resources := range b { + if !strings.HasSuffix(resourceType, ".permissions") && !strings.HasSuffix(resourceType, ".grants") { + continue + } + for resourceName := range resources { + diags = diags.Append(diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("binding %s is not allowed", resourceType), + Detail: fmt.Sprintf( + "bind can only be used for resources directly under the resources block, not for child resources like permissions or grants.\n\n"+ + "To manage permissions or grants:\n"+ + "1. First bind the parent resource (without .permissions or .grants)\n"+ + "2. Then define permissions or grants in your bundle configuration\n\n"+ + "Invalid bind configuration:\n"+ + " bind:\n"+ + " %s:\n"+ + " %s:\n"+ + " id: ...\n\n"+ + "Instead, remove this bind entry and ensure the parent resource is bound.", + resourceType, resourceName), + }) + } + } + + return diags +} diff --git a/bundle/config/bind_test.go b/bundle/config/bind_test.go new file mode 100644 index 00000000000..e5a02a7eceb --- /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 + }{ + {"top-level resource", config.Bind{"jobs": {"foo": {ID: "1"}}}, false}, + {"permissions child", config.Bind{"jobs.permissions": {"foo": {ID: "1"}}}, true}, + {"grants child", config.Bind{"schemas.grants": {"foo": {ID: "1"}}}, true}, + // Substring match must NOT trigger; only the .permissions / .grants suffix does. + {"name containing permissions", config.Bind{"my_permissions_jobs": {"foo": {ID: "1"}}}, false}, + } + 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..e8f10ac9303 --- /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 TestValidateBindResourcesRejectsChildResources(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 allowed") +} 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 e8e14279567..87e144dfa0f 100644 --- a/bundle/deployplan/action.go +++ b/bundle/deployplan/action.go @@ -28,36 +28,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) diff --git a/bundle/deployplan/plan.go b/bundle/deployplan/plan.go index b35357c7c28..fe39bd7e6cb 100644 --- a/bundle/deployplan/plan.go +++ b/bundle/deployplan/plan.go @@ -74,6 +74,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"` NewState *structvar.StructVarJSON `json:"new_state,omitempty"` diff --git a/bundle/direct/apply.go b/bundle/direct/apply.go index e7186f64670..659537904e4 100644 --- a/bundle/direct/apply.go +++ b/bundle/direct/apply.go @@ -47,6 +47,38 @@ func (d *DeploymentUnit) Deploy(ctx context.Context, db *dstate.DeploymentState, } } +// DeclarativeBind brings an existing workspace resource under bundle management. +// For Bind, it just persists state with the bind ID. For BindAndUpdate, it also +// applies the configured field changes to the remote resource. +func (d *DeploymentUnit) DeclarativeBind(ctx context.Context, db *dstate.DeploymentState, bindID string, newState any, actionType deployplan.ActionType, planEntry *deployplan.PlanEntry) error { + if actionType == deployplan.BindAndUpdate { + if !d.Adapter.HasDoUpdate() { + return fmt.Errorf("internal error: DoUpdate not implemented for resource %s", d.ResourceKey) + } + + remoteState, err := d.Adapter.DoUpdate(ctx, bindID, newState, planEntry) + if err != nil { + return fmt.Errorf("updating bound resource id=%s: %w", bindID, err) + } + + err = d.SetRemoteState(remoteState) + if err != nil { + return err + } + + log.Infof(ctx, "Bound and updated %s id=%s", d.ResourceKey, bindID) + } else { + log.Infof(ctx, "Bound %s id=%s", d.ResourceKey, bindID) + } + + err := db.SaveState(d.ResourceKey, bindID, newState, d.DependsOn) + if err != nil { + return fmt.Errorf("saving state id=%s: %w", bindID, err) + } + + return nil +} + func (d *DeploymentUnit) Create(ctx context.Context, db *dstate.DeploymentState, newState any) error { newID, remoteState, err := d.Adapter.DoCreate(ctx, newState) if err != nil { diff --git a/bundle/direct/bundle_apply.go b/bundle/direct/bundle_apply.go index a7f3ee65fc2..4a8a39365eb 100644 --- a/bundle/direct/bundle_apply.go +++ b/bundle/direct/bundle_apply.go @@ -119,6 +119,8 @@ func (b *DeploymentBundle) Apply(ctx context.Context, client *databricks.Workspa return false } err = b.StateDB.SaveState(resourceKey, id, sv.Value, entry.DependsOn) + } else if action.IsBind() { + err = d.DeclarativeBind(ctx, &b.StateDB, entry.BindID, sv.Value, action, entry) } else { // TODO: redo calcDiff to downgrade planned action if possible (?) err = d.Deploy(ctx, &b.StateDB, sv.Value, action, entry) diff --git a/bundle/direct/bundle_plan_bind.go b/bundle/direct/bundle_plan_bind.go new file mode 100644 index 00000000000..faf009234eb --- /dev/null +++ b/bundle/direct/bundle_plan_bind.go @@ -0,0 +1,263 @@ +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/dyn" + "github.com/databricks/cli/libs/logdiag" + "github.com/databricks/cli/libs/structs/structdiff" +) + +// ApplyBindToPlan layers declarative bind blocks onto a plan that has already been +// produced by CalculatePlan. It validates the bind config and overrides plan +// entries for resources that are being bound for the first time. +// +// For each bind entry: +// - If the resource has no state, override the planned Create with Bind or +// BindAndUpdate by reading the remote resource and diffing against config. +// - If the resource is already in state under the same ID, the bind is a no-op +// (a previously-bound resource deploys normally). +// - If the resource is in state under a different ID, surface an error. +func (b *DeploymentBundle) ApplyBindToPlan(ctx context.Context, configRoot *config.Root, bindConfig config.Bind) error { + if bindConfig.IsEmpty() { + return nil + } + + if b.Plan == nil { + return errors.New("internal error: ApplyBindToPlan called before CalculatePlan") + } + + if !b.validateBindConfig(ctx, configRoot, bindConfig) { + return errors.New("bind validation failed") + } + + bindConfig.ForEach(func(resourceType, resourceName, bindID string) { + resourceKey := "resources." + resourceType + "." + resourceName + entry := b.Plan.Plan[resourceKey] + + dbentry, hasEntry := b.StateDB.GetResourceEntry(resourceKey) + if hasEntry { + if dbentry.ID != bindID { + logdiag.LogError(ctx, fmt.Errorf("%s: resource already bound to ID %q, cannot bind as %q; remove the bind block or unbind the existing resource", resourceKey, dbentry.ID, bindID)) + return + } + // IDs match: leave the plan entry as-is so the resource deploys normally. + return + } + + entry.BindID = bindID + adapter, err := b.getAdapterForKey(resourceKey) + if err != nil { + logdiag.LogError(ctx, fmt.Errorf("cannot plan %s: getting adapter: %w", resourceKey, err)) + return + } + + b.handleBindPlan(ctx, resourceKey, entry, adapter) + }) + + if logdiag.HasError(ctx) { + return errors.New("bind planning failed") + } + + return nil +} + +// 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 := dyn.NewPath(dyn.Key("targets"), dyn.Key(targetName), dyn.Key("bind"), dyn.Key(resourceType), dyn.Key(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.GetLocations(bindPath.String()), + Paths: []dyn.Path{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 := dyn.NewPath(dyn.Key("targets"), dyn.Key(targetName), dyn.Key("bind"), dyn.Key(resourceType), dyn.Key(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.GetLocations(bindPath.String()), + Paths: []dyn.Path{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 := adapter.DoRead(ctx, bindID) + if err != nil { + if isResourceGone(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 + } + + remoteDiff, err := structdiff.GetStructDiff(remoteStateComparable, sv.Value, 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, 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 2f28ca27596..921c7f585bf 100644 --- a/bundle/internal/schema/annotations.yml +++ b/bundle/internal/schema/annotations.yml @@ -23,6 +23,10 @@ github.com/databricks/cli/bundle/config.ArtifactFile: "source": "description": |- Required. The artifact source file. +github.com/databricks/cli/bundle/config.BindResource: + "id": + "description": |- + The ID of the existing workspace resource to bind. github.com/databricks/cli/bundle/config.Bundle: "cluster_id": "description": |- @@ -372,6 +376,9 @@ github.com/databricks/cli/bundle/config.Target: "artifacts": "description": |- The artifacts to include in the target deployment. + "bind": + "description": |- + The existing workspace resources to bind into bundle management for this target. "bundle": "description": |- The bundle attributes when deploying to this target. diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index 38389b9adb2..844b0af709c 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -256,12 +256,25 @@ func Deploy(ctx context.Context, b *bundle.Bundle, outputHandler sync.OutputHand } func RunPlan(ctx context.Context, b *bundle.Bundle, engine engine.EngineType) *deployplan.Plan { + // Bind blocks rely on direct-engine planning primitives (DoRead, RemapState), + // so reject them early when the user is on the terraform engine. + if !engine.IsDirect() && b.Target != nil && !b.Target.Bind.IsEmpty() { + logdiag.LogError(ctx, errors.New("bind blocks in the target configuration are only supported with the direct deployment engine; set DATABRICKS_BUNDLE_ENGINE=direct or remove the bind blocks")) + return nil + } + if engine.IsDirect() { plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config) if err != nil { logdiag.LogError(ctx, err) return nil } + if b.Target != nil && !b.Target.Bind.IsEmpty() { + if err := b.DeploymentBundle.ApplyBindToPlan(ctx, &b.Config, b.Target.Bind); err != nil { + logdiag.LogError(ctx, err) + return nil + } + } return plan } diff --git a/bundle/phases/destroy.go b/bundle/phases/destroy.go index 4abc6140e45..741ab8351d2 100644 --- a/bundle/phases/destroy.go +++ b/bundle/phases/destroy.go @@ -3,9 +3,13 @@ package phases import ( "context" "errors" + "fmt" "net/http" + "slices" + "strings" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/engine" "github.com/databricks/cli/bundle/config/mutator" "github.com/databricks/cli/bundle/deploy/files" @@ -13,6 +17,7 @@ import ( "github.com/databricks/cli/bundle/deploy/terraform" "github.com/databricks/cli/bundle/deployplan" "github.com/databricks/cli/bundle/direct" + "github.com/databricks/cli/bundle/direct/dstate" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" "github.com/databricks/cli/libs/logdiag" @@ -155,6 +160,26 @@ func destroyCore(ctx context.Context, b *bundle.Bundle, plan *deployplan.Plan, e } } +// errIfBoundResourcesInState rejects a destroy when any bind block currently +// matches a resource in state: that resource was imported, not created by the +// bundle, and a blanket destroy would delete a pre-existing workspace resource. +func errIfBoundResourcesInState(stateDB *dstate.DeploymentState, bindConfig config.Bind) error { + var boundInState []string + bindConfig.ForEach(func(resourceType, resourceName, bindID string) { + key := "resources." + resourceType + "." + resourceName + if entry, ok := stateDB.Data.State[key]; ok && entry.ID == bindID { + boundInState = append(boundInState, key) + } + }) + + if len(boundInState) == 0 { + return nil + } + + slices.Sort(boundInState) + return fmt.Errorf("cannot destroy with bind blocks that reference resources in the deployment state: %s; remove the bind blocks from the target configuration or run 'bundle deployment unbind' before destroying", strings.Join(boundInState, ", ")) +} + // The destroy phase deletes artifacts and resources. func Destroy(ctx context.Context, b *bundle.Bundle, engine engine.EngineType) { log.Info(ctx, "Phase: destroy") @@ -199,6 +224,16 @@ func Destroy(ctx context.Context, b *bundle.Bundle, engine engine.EngineType) { var plan *deployplan.Plan if engine.IsDirect() { + // Refuse to destroy when bind blocks point at resources that are currently + // in state: those are pre-existing workspace resources the user imported, + // and destroying them would silently delete data the bundle did not create. + if b.Target != nil && !b.Target.Bind.IsEmpty() { + if err := errIfBoundResourcesInState(&b.DeploymentBundle.StateDB, b.Target.Bind); err != nil { + logdiag.LogError(ctx, err) + return + } + } + plan, err = b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), nil) if err != nil { logdiag.LogError(ctx, err) diff --git a/bundle/phases/initialize.go b/bundle/phases/initialize.go index 25bb2f4bd1b..ab26b5c6291 100644 --- a/bundle/phases/initialize.go +++ b/bundle/phases/initialize.go @@ -185,6 +185,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 ee105a6f821..383316a18a6 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -2206,6 +2206,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(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "config.Bundle": { "oneOf": [ { @@ -2632,6 +2653,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" @@ -11552,6 +11577,20 @@ } ] }, + "config.BindResource": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/github.com/databricks/cli/bundle/config.BindResource" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "config.Command": { "oneOf": [ { @@ -11598,6 +11637,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(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + } + } + } + } + } + }, "string": { "oneOf": [ { diff --git a/cmd/bundle/deployment/migrate.go b/cmd/bundle/deployment/migrate.go index 5020d88e73a..400fb248e06 100644 --- a/cmd/bundle/deployment/migrate.go +++ b/cmd/bundle/deployment/migrate.go @@ -166,6 +166,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 e3dd63929ed..ec5854afbc0 100644 --- a/cmd/bundle/plan.go +++ b/cmd/bundle/plan.go @@ -72,6 +72,7 @@ It is useful for previewing changes before running 'bundle deploy'.`, updateCount := 0 deleteCount := 0 unchangedCount := 0 + bindCount := 0 for _, change := range plan.GetActions() { switch change.ActionType { @@ -85,6 +86,11 @@ It is useful for previewing changes before running 'bundle deploy'.`, // A recreate counts as both a delete and a create deleteCount++ createCount++ + case deployplan.Bind: + bindCount++ + case deployplan.BindAndUpdate: + bindCount++ + updateCount++ case deployplan.Skip, deployplan.Undefined: unchangedCount++ } @@ -95,7 +101,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 := createCount + updateCount + deleteCount + totalChanges := createCount + updateCount + deleteCount + bindCount if totalChanges > 0 { // Print all actions in the order they were processed for _, action := range plan.GetActions() { @@ -103,12 +109,21 @@ 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() { + entry := plan.Plan[action.ResourceKey] + fmt.Fprintf(out, "%s %s (id: %s)\n", action.ActionType.StringShort(), key, entry.BindID) + } else { + fmt.Fprintf(out, "%s %s\n", action.ActionType.StringShort(), key) + } } fmt.Fprintln(out) } // 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\n", createCount, updateCount, deleteCount, unchangedCount) + if bindCount > 0 { + fmt.Fprintf(out, "Plan: %d to add, %d to change, %d to delete, %d unchanged, %d to bind\n", createCount, updateCount, deleteCount, unchangedCount, bindCount) + } else { + fmt.Fprintf(out, "Plan: %d to add, %d to change, %d to delete, %d unchanged\n", createCount, updateCount, deleteCount, unchangedCount) + } case flags.OutputJSON: buf, err := json.MarshalIndent(plan, "", " ") if err != nil { diff --git a/libs/dyn/convert/normalize.go b/libs/dyn/convert/normalize.go index 79cfee37441..ae496c3a5c4 100644 --- a/libs/dyn/convert/normalize.go +++ b/libs/dyn/convert/normalize.go @@ -116,6 +116,21 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen } } + // Bind is only valid under a target. Authors sometimes try to put it at + // the root, which would otherwise surface here as a generic "unknown + // field" warning that's easy to miss. Emit a targeted error so the + // misplaced block fails the build with actionable guidance. + if fieldName == "bind" && len(path) == 0 { + diags = diags.Append(diag.Diagnostic{ + Severity: diag.Error, + Summary: "bind blocks are not allowed at the root level", + Detail: "bind blocks must be defined within a target. Move the bind configuration under targets..bind", + Locations: pk.Locations(), + Paths: []dyn.Path{path}, + }) + continue + } + diags = diags.Append(diag.Diagnostic{ Severity: diag.Warning, Summary: "unknown field: " + fieldName, diff --git a/libs/dyn/convert/normalize_test.go b/libs/dyn/convert/normalize_test.go index 7eb50a13b67..371644f1de4 100644 --- a/libs/dyn/convert/normalize_test.go +++ b/libs/dyn/convert/normalize_test.go @@ -90,6 +90,25 @@ func TestNormalizeStructUnknownField(t *testing.T) { }, vout.AsAny()) } +func TestNormalizeStructBindAtRootIsError(t *testing.T) { + // "bind" at the root level is a common mistake — without this special case it + // would silently degrade to an "unknown field" warning. Make sure we surface a + // dedicated error with location info instead. + type Tmp struct { + Foo string `json:"foo"` + } + + var typ Tmp + + m := dyn.NewMapping() + m.SetLoc("bind", []dyn.Location{{File: "databricks.yml", Line: 1, Column: 1}}, dyn.V(map[string]dyn.Value{})) + + _, diags := Normalize(typ, dyn.V(m)) + assert.Len(t, diags, 1) + assert.Equal(t, diag.Error, diags[0].Severity) + assert.Equal(t, "bind blocks are not allowed at the root level", diags[0].Summary) +} + func TestNormalizeStructNil(t *testing.T) { type Tmp struct { Foo string `json:"foo"` From 20d1033dc2b96db0823fc8dfda0442723c93de20 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 13 Jul 2026 12:38:51 +0000 Subject: [PATCH 02/24] Address review: bind config-level duplicate-ID check + WaitAfterUpdate - validateBindConfig now rejects two bind blocks pointing at the same workspace ID. Previously duplicate IDs were only checked against state, so two first-time binds could create two state entries managing one remote resource. - DeclarativeBind's BindAndUpdate path now defers to Update, so DoUpdate runs with transient retries and WaitAfterUpdate is invoked. Async resources rely on that hook; the previous inline path skipped it. - Add acceptance test duplicate-bind-id-config for the new validation. Co-authored-by: Isaac --- .../duplicate-bind-id-config/databricks.yml | 36 +++++++++++++++++++ .../bind/duplicate-bind-id-config/hello.py | 1 + .../duplicate-bind-id-config/out.test.toml | 3 ++ .../bind/duplicate-bind-id-config/output.txt | 10 ++++++ .../bind/duplicate-bind-id-config/script | 3 ++ bundle/direct/apply.go | 22 ++++-------- bundle/direct/bundle_plan_bind.go | 25 +++++++++++++ 7 files changed, 84 insertions(+), 16 deletions(-) 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 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..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/duplicate-bind-id-config/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] 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/bundle/direct/apply.go b/bundle/direct/apply.go index 6926e34c999..92bff5f56c2 100644 --- a/bundle/direct/apply.go +++ b/bundle/direct/apply.go @@ -51,29 +51,19 @@ func (d *DeploymentUnit) Deploy(ctx context.Context, db *dstate.DeploymentState, } // DeclarativeBind brings an existing workspace resource under bundle management. -// For Bind, it just persists state with the bind ID. For BindAndUpdate, it also -// applies the configured field changes to the remote resource. +// For Bind, it just persists state with the bind ID. For BindAndUpdate, it defers +// to Update so the configured field changes go through the same DoUpdate retry and +// WaitAfterUpdate path as a normal update (async resources rely on that hook). func (d *DeploymentUnit) DeclarativeBind(ctx context.Context, db *dstate.DeploymentState, bindID string, newState any, actionType deployplan.ActionType, planEntry *deployplan.PlanEntry) error { if actionType == deployplan.BindAndUpdate { - if !d.Adapter.HasDoUpdate() { - return fmt.Errorf("internal error: DoUpdate not implemented for resource %s", d.ResourceKey) - } - - remoteState, err := d.Adapter.DoUpdate(ctx, bindID, newState, planEntry) - if err != nil { - return fmt.Errorf("updating bound resource id=%s: %w", bindID, err) - } - - err = d.SetRemoteState(remoteState) - if err != nil { + if err := d.Update(ctx, db, bindID, newState, planEntry); err != nil { return err } - log.Infof(ctx, "Bound and updated %s id=%s", d.ResourceKey, bindID) - } else { - log.Infof(ctx, "Bound %s id=%s", d.ResourceKey, bindID) + return nil } + log.Infof(ctx, "Bound %s id=%s", d.ResourceKey, bindID) err := db.SaveState(d.ResourceKey, bindID, newState, d.DependsOn) if err != nil { return fmt.Errorf("saving state id=%s: %w", bindID, err) diff --git a/bundle/direct/bundle_plan_bind.go b/bundle/direct/bundle_plan_bind.go index 27b38f74af4..c34bb263eb6 100644 --- a/bundle/direct/bundle_plan_bind.go +++ b/bundle/direct/bundle_plan_bind.go @@ -93,6 +93,31 @@ func (b *DeploymentBundle) validateBindConfig(ctx context.Context, configRoot *c 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 := dyn.NewPath(dyn.Key("targets"), dyn.Key(targetName), dyn.Key("bind"), dyn.Key(resourceType), dyn.Key(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.GetLocations(bindPath.String()), + Paths: []dyn.Path{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. From 944e8b9d21e31882d3b213ae3aa241d1ffe4b07f Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 13 Jul 2026 13:13:58 +0000 Subject: [PATCH 03/24] Address review: closed-list bind validation, split Bind/BindAndUpdate, drop root-bind error - Bind.Validate now checks resource types against the closed allowlist from config.SupportedResources() instead of an open blocklist of *.permissions / *.grants. Only types valid under the resources block are bindable; anything else (typo, child key, etc.) is rejected with a "not supported" message. Drops the child-resource terminology. - Split DeclarativeBind into Bind and BindAndUpdate DeploymentUnit methods, dispatched from the apply loop like the other action types. - Remove the special root-level bind error in normalize.go; a root bind block is just an ignored unknown-field warning now. - Move the changelog entry from NEXT_CHANGELOG.md to a .nextchanges/bundles/ fragment (new convention, #5534). - Regenerate affected acceptance goldens (bind-permissions, invalid-resource-type, top-level-bind). Co-authored-by: Isaac --- .nextchanges/bundles/4630.md | 1 + NEXT_CHANGELOG.md | 1 - .../deploy/bind/bind-permissions/output.txt | 16 +------- .../bind/invalid-resource-type/output.txt | 5 +-- .../deploy/bind/top-level-bind/output.txt | 13 +++--- .../bundle/deploy/bind/top-level-bind/script | 4 +- bundle/config/bind.go | 41 +++++++------------ bundle/config/bind_test.go | 6 +-- .../mutator/validate_bind_resources_test.go | 4 +- bundle/direct/apply.go | 32 ++++++++------- bundle/direct/bundle_apply.go | 9 ++-- libs/dyn/convert/normalize.go | 15 ------- libs/dyn/convert/normalize_test.go | 19 --------- 13 files changed, 59 insertions(+), 107 deletions(-) create mode 100644 .nextchanges/bundles/4630.md 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/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 6c6c05eefc0..f2b99b3591d 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -9,7 +9,6 @@ * experimental `ssh connect`: bare `python`/`pip` in an interactive session now resolve to the environment interpreter (`$DATABRICKS_VIRTUAL_ENV`) instead of the system or cluster-libraries interpreter, so packages installed in the environment are importable without extra setup. The interactive shell is now non-login (`bash -i`) and the server seeds a `~/.bashrc` snippet that re-prepends the environment's bin directory to `PATH` ([#5888](https://github.com/databricks/cli/pull/5888)). ### Bundles -* 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)). * Added an `env:` section to `scripts.` for declaring environment variables that may reference `${bundle.*}`, `${workspace.*}`, and `${var.*}` ([#4179](https://github.com/databricks/cli/issues/4179)). diff --git a/acceptance/bundle/deploy/bind/bind-permissions/output.txt b/acceptance/bundle/deploy/bind/bind-permissions/output.txt index 3510df058e9..1150649418d 100644 --- a/acceptance/bundle/deploy/bind/bind-permissions/output.txt +++ b/acceptance/bundle/deploy/bind/bind-permissions/output.txt @@ -1,20 +1,8 @@ >>> musterr [CLI] bundle validate -Error: binding jobs.permissions is not allowed +Error: binding "jobs.permissions" is not supported -bind can only be used for resources directly under the resources block, not for child resources like permissions or grants. - -To manage permissions or grants: -1. First bind the parent resource (without .permissions or .grants) -2. Then define permissions or grants in your bundle configuration - -Invalid bind configuration: - bind: - jobs.permissions: - foo: - id: ... - -Instead, remove this bind entry and ensure the parent resource is bound. +"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 diff --git a/acceptance/bundle/deploy/bind/invalid-resource-type/output.txt b/acceptance/bundle/deploy/bind/invalid-resource-type/output.txt index d06b74c8f14..eb33b3f42ba 100644 --- a/acceptance/bundle/deploy/bind/invalid-resource-type/output.txt +++ b/acceptance/bundle/deploy/bind/invalid-resource-type/output.txt @@ -1,7 +1,6 @@ >>> musterr [CLI] bundle plan -Error: bind block references undefined resource "resources.foobar.foo"; define it in the resources section or remove the bind block - at targets.default.bind.foobar.foo +Error: binding "foobar" is not supported -Error: bind validation failed +"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/top-level-bind/output.txt b/acceptance/bundle/deploy/bind/top-level-bind/output.txt index 9425c8feb52..ee8fd54ee74 100644 --- a/acceptance/bundle/deploy/bind/top-level-bind/output.txt +++ b/acceptance/bundle/deploy/bind/top-level-bind/output.txt @@ -1,9 +1,12 @@ ->>> musterr [CLI] bundle validate -Error: bind blocks are not allowed at the root level +>>> [CLI] bundle validate +Warning: unknown field: bind in databricks.yml:4:1 -bind blocks must be defined within a target. Move the bind configuration under targets..bind +Name: test-bind-top-level +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bind-top-level/default - -Found 1 error +Found 1 warning diff --git a/acceptance/bundle/deploy/bind/top-level-bind/script b/acceptance/bundle/deploy/bind/top-level-bind/script index 44dbe24759c..fd58a501146 100644 --- a/acceptance/bundle/deploy/bind/top-level-bind/script +++ b/acceptance/bundle/deploy/bind/top-level-bind/script @@ -1 +1,3 @@ -trace musterr $CLI bundle validate +# 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/bundle/config/bind.go b/bundle/config/bind.go index 943f3d9582a..1e8ec9aa9d4 100644 --- a/bundle/config/bind.go +++ b/bundle/config/bind.go @@ -3,7 +3,6 @@ package config import ( "fmt" "slices" - "strings" "github.com/databricks/cli/libs/diag" ) @@ -51,36 +50,24 @@ func (b Bind) IsEmpty() bool { return true } -// Validate rejects bind blocks that target child resources (e.g. "jobs.permissions"). -// The direct engine exposes child resources as full keys like "jobs.permissions" in its -// resource registry, but they are not bindable on their own — bind the parent resource -// and let the bundle manage the child entries declaratively. +// 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 - for resourceType, resources := range b { - if !strings.HasSuffix(resourceType, ".permissions") && !strings.HasSuffix(resourceType, ".grants") { - continue + supported := SupportedResources() + b.ForEach(func(resourceType, resourceName, bindID string) { + if _, ok := supported[resourceType]; ok { + return } - for resourceName := range resources { - diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("binding %s is not allowed", resourceType), - Detail: fmt.Sprintf( - "bind can only be used for resources directly under the resources block, not for child resources like permissions or grants.\n\n"+ - "To manage permissions or grants:\n"+ - "1. First bind the parent resource (without .permissions or .grants)\n"+ - "2. Then define permissions or grants in your bundle configuration\n\n"+ - "Invalid bind configuration:\n"+ - " bind:\n"+ - " %s:\n"+ - " %s:\n"+ - " id: ...\n\n"+ - "Instead, remove this bind entry and ensure the parent resource is bound.", - resourceType, resourceName), - }) - } - } + 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 index e5a02a7eceb..88e2aacb9cf 100644 --- a/bundle/config/bind_test.go +++ b/bundle/config/bind_test.go @@ -50,11 +50,11 @@ func TestBindValidate(t *testing.T) { bind config.Bind wantError bool }{ - {"top-level resource", config.Bind{"jobs": {"foo": {ID: "1"}}}, false}, + {"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}, - // Substring match must NOT trigger; only the .permissions / .grants suffix does. - {"name containing permissions", config.Bind{"my_permissions_jobs": {"foo": {ID: "1"}}}, false}, + {"unknown resource type", config.Bind{"my_permissions_jobs": {"foo": {ID: "1"}}}, true}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { diff --git a/bundle/config/mutator/validate_bind_resources_test.go b/bundle/config/mutator/validate_bind_resources_test.go index e8f10ac9303..b3d5e63d799 100644 --- a/bundle/config/mutator/validate_bind_resources_test.go +++ b/bundle/config/mutator/validate_bind_resources_test.go @@ -26,7 +26,7 @@ func TestValidateBindResourcesAcceptsTopLevel(t *testing.T) { assert.NoError(t, diags.Error()) } -func TestValidateBindResourcesRejectsChildResources(t *testing.T) { +func TestValidateBindResourcesRejectsUnsupportedType(t *testing.T) { b := &bundle.Bundle{ Target: &config.Target{ Bind: config.Bind{ @@ -36,5 +36,5 @@ func TestValidateBindResourcesRejectsChildResources(t *testing.T) { } diags := bundle.Apply(t.Context(), b, ValidateBindResources()) assert.Error(t, diags.Error()) - assert.Contains(t, diags[0].Summary, "binding jobs.permissions is not allowed") + assert.Contains(t, diags[0].Summary, `binding "jobs.permissions" is not supported`) } diff --git a/bundle/direct/apply.go b/bundle/direct/apply.go index 92bff5f56c2..8ee6da00ecb 100644 --- a/bundle/direct/apply.go +++ b/bundle/direct/apply.go @@ -50,25 +50,29 @@ func (d *DeploymentUnit) Deploy(ctx context.Context, db *dstate.DeploymentState, } } -// DeclarativeBind brings an existing workspace resource under bundle management. -// For Bind, it just persists state with the bind ID. For BindAndUpdate, it defers -// to Update so the configured field changes go through the same DoUpdate retry and -// WaitAfterUpdate path as a normal update (async resources rely on that hook). -func (d *DeploymentUnit) DeclarativeBind(ctx context.Context, db *dstate.DeploymentState, bindID string, newState any, actionType deployplan.ActionType, planEntry *deployplan.PlanEntry) error { - if actionType == deployplan.BindAndUpdate { - if err := d.Update(ctx, db, bindID, newState, planEntry); err != nil { - return err - } - log.Infof(ctx, "Bound and updated %s id=%s", d.ResourceKey, bindID) - return nil +// 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(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, bindID) - err := db.SaveState(d.ResourceKey, bindID, newState, d.DependsOn) + 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 defers to Update so the update goes through the same +// DoUpdate retry and WaitAfterUpdate path as a normal update (async resources rely +// on that hook). +func (d *DeploymentUnit) BindAndUpdate(ctx context.Context, db *dstate.DeploymentState, id string, newState any, planEntry *deployplan.PlanEntry) error { + err := d.Update(ctx, db, id, newState, planEntry) if err != nil { - return fmt.Errorf("saving state id=%s: %w", bindID, err) + return err } + log.Infof(ctx, "Bound and updated %s id=%s", d.ResourceKey, id) return nil } diff --git a/bundle/direct/bundle_apply.go b/bundle/direct/bundle_apply.go index f6d0055bc9f..561a4d99173 100644 --- a/bundle/direct/bundle_apply.go +++ b/bundle/direct/bundle_apply.go @@ -110,9 +110,12 @@ func (b *DeploymentBundle) Apply(ctx context.Context, client *databricks.Workspa return false } - if action.IsBind() { - err = d.DeclarativeBind(ctx, &b.StateDB, entry.BindID, sv.Value, action, entry) - } else { + switch { + case action == deployplan.Bind: + err = d.Bind(ctx, &b.StateDB, entry.BindID, sv.Value) + case action == deployplan.BindAndUpdate: + err = d.BindAndUpdate(ctx, &b.StateDB, entry.BindID, sv.Value, entry) + default: // TODO: redo calcDiff to downgrade planned action if possible (?) err = d.Deploy(ctx, &b.StateDB, sv.Value, action, entry) } diff --git a/libs/dyn/convert/normalize.go b/libs/dyn/convert/normalize.go index ae496c3a5c4..79cfee37441 100644 --- a/libs/dyn/convert/normalize.go +++ b/libs/dyn/convert/normalize.go @@ -116,21 +116,6 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen } } - // Bind is only valid under a target. Authors sometimes try to put it at - // the root, which would otherwise surface here as a generic "unknown - // field" warning that's easy to miss. Emit a targeted error so the - // misplaced block fails the build with actionable guidance. - if fieldName == "bind" && len(path) == 0 { - diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "bind blocks are not allowed at the root level", - Detail: "bind blocks must be defined within a target. Move the bind configuration under targets..bind", - Locations: pk.Locations(), - Paths: []dyn.Path{path}, - }) - continue - } - diags = diags.Append(diag.Diagnostic{ Severity: diag.Warning, Summary: "unknown field: " + fieldName, diff --git a/libs/dyn/convert/normalize_test.go b/libs/dyn/convert/normalize_test.go index 28e7a40a61b..cf8ca061733 100644 --- a/libs/dyn/convert/normalize_test.go +++ b/libs/dyn/convert/normalize_test.go @@ -90,25 +90,6 @@ func TestNormalizeStructUnknownField(t *testing.T) { }, vout.AsAny()) } -func TestNormalizeStructBindAtRootIsError(t *testing.T) { - // "bind" at the root level is a common mistake — without this special case it - // would silently degrade to an "unknown field" warning. Make sure we surface a - // dedicated error with location info instead. - type Tmp struct { - Foo string `json:"foo"` - } - - var typ Tmp - - m := dyn.NewMapping() - m.SetLoc("bind", []dyn.Location{{File: "databricks.yml", Line: 1, Column: 1}}, dyn.V(map[string]dyn.Value{})) - - _, diags := Normalize(typ, dyn.V(m)) - assert.Len(t, diags, 1) - assert.Equal(t, diag.Error, diags[0].Severity) - assert.Equal(t, "bind blocks are not allowed at the root level", diags[0].Summary) -} - func TestNormalizeStructNil(t *testing.T) { type Tmp struct { Foo string `json:"foo"` From 618ca922cfef5b97224ce47dbd08fc2d9a115921 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 13 Jul 2026 13:26:48 +0000 Subject: [PATCH 04/24] Fold bind planning into CalculatePlan Previously ApplyBindToPlan ran as a separate step after CalculatePlan, re-reading remote state for bound resources with its own error handling. Since binding a resource is just the "resource exists remotely under a known ID but isn't in state yet" variant of normal planning, fold it in: - CalculatePlan takes a bindConfig parameter. Cross-resource bind validation runs once up front; the per-resource bind flow (handleBindPlan) runs inside the DAG walk, in the not-in-state branch, instead of defaulting to Create. - Bound-resource remote reads now go through retryOnTransient and the parallel DAG walk like every other resource read. - The "already in state under a different ID" check moved into validateBindConfig (it is cross-resource, so it stays out of the parallel closure). - Remove ApplyBindToPlan; update all CalculatePlan callers. Bind planning errors now surface under the shared "planning failed" summary instead of "bind planning failed" (goldens regenerated); the detailed per-resource messages are unchanged. Co-authored-by: Isaac --- .../deploy/bind/recreate-blocked/output.txt | 2 +- .../deploy/bind/resource-not-found/output.txt | 2 +- bundle/direct/bind.go | 4 +- bundle/direct/bundle_plan.go | 27 ++++++- bundle/direct/bundle_plan_bind.go | 77 ++++++------------- bundle/phases/deploy.go | 12 ++- bundle/phases/destroy.go | 2 +- cmd/bundle/config_remote_sync.go | 2 +- 8 files changed, 59 insertions(+), 69 deletions(-) diff --git a/acceptance/bundle/deploy/bind/recreate-blocked/output.txt b/acceptance/bundle/deploy/bind/recreate-blocked/output.txt index 5ddf2b0be75..168068d9ca0 100644 --- a/acceptance/bundle/deploy/bind/recreate-blocked/output.txt +++ b/acceptance/bundle/deploy/bind/recreate-blocked/output.txt @@ -28,5 +28,5 @@ To resolve this issue, you have two options: # id: -Error: bind planning failed +Error: planning failed diff --git a/acceptance/bundle/deploy/bind/resource-not-found/output.txt b/acceptance/bundle/deploy/bind/resource-not-found/output.txt index 96ef84cc0a8..08ddec1d7d0 100644 --- a/acceptance/bundle/deploy/bind/resource-not-found/output.txt +++ b/acceptance/bundle/deploy/bind/resource-not-found/output.txt @@ -2,5 +2,5 @@ >>> musterr [CLI] bundle plan Error: cannot bind resources.jobs.foo: resource with ID "[NUMID]" does not exist in workspace -Error: bind planning failed +Error: planning failed diff --git a/bundle/direct/bind.go b/bundle/direct/bind.go index 9760ce95666..e02d48653a8 100644 --- a/bundle/direct/bind.go +++ b/bundle/direct/bind.go @@ -114,7 +114,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 @@ -170,7 +170,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 139012423ea..3821b657ab8 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -116,7 +116,11 @@ func (b *DeploymentBundle) InitForApply(ctx context.Context, client *databricks. // 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) @@ -131,6 +135,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 @@ -210,6 +227,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 } diff --git a/bundle/direct/bundle_plan_bind.go b/bundle/direct/bundle_plan_bind.go index c34bb263eb6..4aea26c0efb 100644 --- a/bundle/direct/bundle_plan_bind.go +++ b/bundle/direct/bundle_plan_bind.go @@ -17,60 +17,6 @@ import ( "github.com/databricks/databricks-sdk-go/apierr" ) -// ApplyBindToPlan layers declarative bind blocks onto a plan that has already been -// produced by CalculatePlan. It validates the bind config and overrides plan -// entries for resources that are being bound for the first time. -// -// For each bind entry: -// - If the resource has no state, override the planned Create with Bind or -// BindAndUpdate by reading the remote resource and diffing against config. -// - If the resource is already in state under the same ID, the bind is a no-op -// (a previously-bound resource deploys normally). -// - If the resource is in state under a different ID, surface an error. -func (b *DeploymentBundle) ApplyBindToPlan(ctx context.Context, configRoot *config.Root, bindConfig config.Bind) error { - if bindConfig.IsEmpty() { - return nil - } - - if b.Plan == nil { - return errors.New("internal error: ApplyBindToPlan called before CalculatePlan") - } - - if !b.validateBindConfig(ctx, configRoot, bindConfig) { - return errors.New("bind validation failed") - } - - bindConfig.ForEach(func(resourceType, resourceName, bindID string) { - resourceKey := "resources." + resourceType + "." + resourceName - entry := b.Plan.Plan[resourceKey] - - dbentry, hasEntry := b.StateDB.GetResourceEntry(resourceKey) - if hasEntry { - if dbentry.ID != bindID { - logdiag.LogError(ctx, fmt.Errorf("%s: resource already bound to ID %q, cannot bind as %q; remove the bind block or unbind the existing resource", resourceKey, dbentry.ID, bindID)) - return - } - // IDs match: leave the plan entry as-is so the resource deploys normally. - return - } - - entry.BindID = bindID - adapter, err := b.getAdapterForKey(resourceKey) - if err != nil { - logdiag.LogError(ctx, fmt.Errorf("cannot plan %s: getting adapter: %w", resourceKey, err)) - return - } - - b.handleBindPlan(ctx, resourceKey, entry, adapter) - }) - - if logdiag.HasError(ctx) { - return errors.New("bind planning failed") - } - - return nil -} - // 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 { @@ -93,6 +39,25 @@ func (b *DeploymentBundle) validateBindConfig(ctx context.Context, configRoot *c 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 := dyn.NewPath(dyn.Key("targets"), dyn.Key(targetName), dyn.Key("bind"), dyn.Key(resourceType), dyn.Key(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.GetLocations(bindPath.String()), + Paths: []dyn.Path{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. @@ -158,7 +123,9 @@ func (b *DeploymentBundle) handleBindPlan(ctx context.Context, resourceKey strin bindID := entry.BindID errorPrefix := "cannot bind " + resourceKey - remoteState, err := adapter.DoRead(ctx, bindID) + remoteState, err := retryOnTransient(ctx, 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)) diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index d88410ee29b..23e3d7a80d6 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -272,17 +272,15 @@ func RunPlan(ctx context.Context, b *bundle.Bundle, engine engine.EngineType) *d } if engine.IsDirect() { - plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config) + var bindConfig config.Bind + if b.Target != nil { + bindConfig = b.Target.Bind + } + plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), &b.Config, bindConfig) if err != nil { logdiag.LogError(ctx, err) return nil } - if b.Target != nil && !b.Target.Bind.IsEmpty() { - if err := b.DeploymentBundle.ApplyBindToPlan(ctx, &b.Config, b.Target.Bind); err != nil { - logdiag.LogError(ctx, err) - return nil - } - } if len(b.Select) > 0 { plan.FilterToSelected(b.Select) } diff --git a/bundle/phases/destroy.go b/bundle/phases/destroy.go index 52a3e4bbd1f..ff8365cec6b 100644 --- a/bundle/phases/destroy.go +++ b/bundle/phases/destroy.go @@ -189,7 +189,7 @@ func Destroy(ctx context.Context, b *bundle.Bundle, engine engine.EngineType) { } } - plan, err = b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), nil) + plan, err = b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(ctx), nil, nil) if err != nil { logdiag.LogError(ctx, err) return diff --git a/cmd/bundle/config_remote_sync.go b/cmd/bundle/config_remote_sync.go index 93d9bbb14c3..b4afe4d9750 100644 --- a/cmd/bundle/config_remote_sync.go +++ b/cmd/bundle/config_remote_sync.go @@ -87,7 +87,7 @@ Examples: return err } - 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) From d30ed6e3ac78e771d981b25f8d5dc3654e7b5e39 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 13 Jul 2026 13:45:13 +0000 Subject: [PATCH 05/24] lint: use tagged switch on action in bind dispatch staticcheck QF1002: prefer a tagged switch over switch-true with equality cases. Co-authored-by: Isaac --- bundle/direct/bundle_apply.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bundle/direct/bundle_apply.go b/bundle/direct/bundle_apply.go index 561a4d99173..278e1444cae 100644 --- a/bundle/direct/bundle_apply.go +++ b/bundle/direct/bundle_apply.go @@ -110,10 +110,10 @@ func (b *DeploymentBundle) Apply(ctx context.Context, client *databricks.Workspa return false } - switch { - case action == deployplan.Bind: + switch action { + case deployplan.Bind: err = d.Bind(ctx, &b.StateDB, entry.BindID, sv.Value) - case action == deployplan.BindAndUpdate: + case deployplan.BindAndUpdate: err = d.BindAndUpdate(ctx, &b.StateDB, entry.BindID, sv.Value, entry) default: // TODO: redo calcDiff to downgrade planned action if possible (?) From ad1d13ca7ba8d68d432d1fa8b5a60c8ed4d95cfe Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Wed, 22 Jul 2026 14:42:29 +0200 Subject: [PATCH 06/24] Allow bundle destroy for resources with active bind blocks The design allows `bundle destroy` even when bind blocks reference resources in state: naming destroy explicitly captures the user's intent to tear the deployment down. The bind block guards against *accidental* deletion during a normal deploy, not against a deliberate destroy. Remove errIfBoundResourcesInState and the destroy-time guard, repurpose the destroy-blocked acceptance test as destroy-allowed, and drop the now-unnecessary "remove bind block before destroy" workaround from the basic and bind-and-update tests. Co-authored-by: Isaac --- acceptance/bundle/deploy/bind/basic/script | 18 +-------- .../bundle/deploy/bind/bind-and-update/script | 18 +-------- .../databricks.yml | 2 +- .../hello.py | 0 .../out.test.toml | 0 .../output.txt | 8 +--- .../script | 21 +--------- bundle/phases/destroy.go | 39 +++---------------- 8 files changed, 12 insertions(+), 94 deletions(-) rename acceptance/bundle/deploy/bind/{destroy-blocked => destroy-allowed}/databricks.yml (91%) rename acceptance/bundle/deploy/bind/{destroy-blocked => destroy-allowed}/hello.py (100%) rename acceptance/bundle/deploy/bind/{destroy-blocked => destroy-allowed}/out.test.toml (100%) rename acceptance/bundle/deploy/bind/{destroy-blocked => destroy-allowed}/output.txt (52%) rename acceptance/bundle/deploy/bind/{destroy-blocked => destroy-allowed}/script (50%) diff --git a/acceptance/bundle/deploy/bind/basic/script b/acceptance/bundle/deploy/bind/basic/script index 29ed7d5e6f9..fc56b7caad2 100644 --- a/acceptance/bundle/deploy/bind/basic/script +++ b/acceptance/bundle/deploy/bind/basic/script @@ -17,21 +17,5 @@ trace $CLI bundle plan # Verify state file contains the imported ID trace print_state.py | contains.py "$NEW_JOB_ID" -# Remove bind block before destroy (destroy blocks on active bind blocks) -python3 << 'PYSCRIPT' -import re -with open('databricks.yml', 'r') as f: - content = f.read() - -# Remove the bind block from the target (everything from "bind:" until the next top-level key or EOF) -content = re.sub(r'\n bind:.*', '', content, flags=re.DOTALL) - -with open('databricks.yml', 'w') as f: - f.write(content) -PYSCRIPT - -# Remove .databricks directory that might cache old config -rm -rf .databricks - -# Cleanup +# 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-update/script b/acceptance/bundle/deploy/bind/bind-and-update/script index 080427e7598..1424e8b5b1c 100644 --- a/acceptance/bundle/deploy/bind/bind-and-update/script +++ b/acceptance/bundle/deploy/bind/bind-and-update/script @@ -24,21 +24,5 @@ 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 -# Remove bind block before destroy (destroy blocks on active bind blocks) -python3 << 'PYSCRIPT' -import re -with open('databricks.yml', 'r') as f: - content = f.read() - -# Remove the bind block from the target (everything from "bind:" until the next top-level key or EOF) -content = re.sub(r'\n bind:.*', '', content, flags=re.DOTALL) - -with open('databricks.yml', 'w') as f: - f.write(content) -PYSCRIPT - -# Remove .databricks directory that might cache old config -rm -rf .databricks - -# Cleanup +# Cleanup: destroy is allowed even with an active bind block trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deploy/bind/destroy-blocked/databricks.yml b/acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml similarity index 91% rename from acceptance/bundle/deploy/bind/destroy-blocked/databricks.yml rename to acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml index f60645ff51f..0a0cf4c7693 100644 --- a/acceptance/bundle/deploy/bind/destroy-blocked/databricks.yml +++ b/acceptance/bundle/deploy/bind/destroy-allowed/databricks.yml @@ -1,5 +1,5 @@ bundle: - name: test-bind-destroy-blocked + name: test-bind-destroy-allowed resources: jobs: diff --git a/acceptance/bundle/deploy/bind/destroy-blocked/hello.py b/acceptance/bundle/deploy/bind/destroy-allowed/hello.py similarity index 100% rename from acceptance/bundle/deploy/bind/destroy-blocked/hello.py rename to acceptance/bundle/deploy/bind/destroy-allowed/hello.py diff --git a/acceptance/bundle/deploy/bind/destroy-blocked/out.test.toml b/acceptance/bundle/deploy/bind/destroy-allowed/out.test.toml similarity index 100% rename from acceptance/bundle/deploy/bind/destroy-blocked/out.test.toml rename to acceptance/bundle/deploy/bind/destroy-allowed/out.test.toml diff --git a/acceptance/bundle/deploy/bind/destroy-blocked/output.txt b/acceptance/bundle/deploy/bind/destroy-allowed/output.txt similarity index 52% rename from acceptance/bundle/deploy/bind/destroy-blocked/output.txt rename to acceptance/bundle/deploy/bind/destroy-allowed/output.txt index 9f8ca5a5c60..33f50c0cf8e 100644 --- a/acceptance/bundle/deploy/bind/destroy-blocked/output.txt +++ b/acceptance/bundle/deploy/bind/destroy-allowed/output.txt @@ -1,19 +1,15 @@ >>> [CLI] bundle deploy --auto-approve -Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-destroy-blocked/default/files... +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bind-destroy-allowed/default/files... Deploying resources... Updating deployment state... Deployment complete! ->>> musterr [CLI] bundle destroy --auto-approve -Error: cannot destroy with bind blocks that reference resources in the deployment state: resources.jobs.foo; remove the bind blocks from the target configuration or run 'bundle deployment unbind' before destroying - - >>> [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-blocked/default +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bind-destroy-allowed/default Deleting files... Destroy complete! diff --git a/acceptance/bundle/deploy/bind/destroy-blocked/script b/acceptance/bundle/deploy/bind/destroy-allowed/script similarity index 50% rename from acceptance/bundle/deploy/bind/destroy-blocked/script rename to acceptance/bundle/deploy/bind/destroy-allowed/script index 508cbc0a043..75d366070f9 100644 --- a/acceptance/bundle/deploy/bind/destroy-blocked/script +++ b/acceptance/bundle/deploy/bind/destroy-allowed/script @@ -8,23 +8,6 @@ update_file.py databricks.yml 'PLACEHOLDER_JOB_ID' "$NEW_JOB_ID" # Deploy to bind the resource into state trace $CLI bundle deploy --auto-approve -# Try to destroy - should fail because bind blocks reference resources in state -trace musterr $CLI bundle destroy --auto-approve - -# Remove bind block and destroy to clean up -python3 << 'PYSCRIPT' -import re -with open('databricks.yml', 'r') as f: - content = f.read() - -# Remove the bind block from the target (everything from "bind:" until the next top-level key or EOF) -content = re.sub(r'\n bind:.*', '', content, flags=re.DOTALL) - -with open('databricks.yml', 'w') as f: - f.write(content) -PYSCRIPT - -# Remove .databricks directory that might cache old config -rm -rf .databricks - +# 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/bundle/phases/destroy.go b/bundle/phases/destroy.go index ff8365cec6b..5ca6c13d121 100644 --- a/bundle/phases/destroy.go +++ b/bundle/phases/destroy.go @@ -3,20 +3,16 @@ package phases import ( "context" "errors" - "fmt" "net/http" "slices" - "strings" "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/engine" "github.com/databricks/cli/bundle/config/mutator" "github.com/databricks/cli/bundle/deploy/files" "github.com/databricks/cli/bundle/deploy/lock" "github.com/databricks/cli/bundle/deploy/terraform" "github.com/databricks/cli/bundle/deployplan" - "github.com/databricks/cli/bundle/direct/dstate" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/log" @@ -115,26 +111,6 @@ func destroyCore(ctx context.Context, b *bundle.Bundle, plan *deployplan.Plan, e } } -// errIfBoundResourcesInState rejects a destroy when any bind block currently -// matches a resource in state: that resource was imported, not created by the -// bundle, and a blanket destroy would delete a pre-existing workspace resource. -func errIfBoundResourcesInState(stateDB *dstate.DeploymentState, bindConfig config.Bind) error { - var boundInState []string - bindConfig.ForEach(func(resourceType, resourceName, bindID string) { - key := "resources." + resourceType + "." + resourceName - if entry, ok := stateDB.Data.State[key]; ok && entry.ID == bindID { - boundInState = append(boundInState, key) - } - }) - - if len(boundInState) == 0 { - return nil - } - - slices.Sort(boundInState) - return fmt.Errorf("cannot destroy with bind blocks that reference resources in the deployment state: %s; remove the bind blocks from the target configuration or run 'bundle deployment unbind' before destroying", strings.Join(boundInState, ", ")) -} - // The destroy phase deletes artifacts and resources. func Destroy(ctx context.Context, b *bundle.Bundle, engine engine.EngineType) { log.Info(ctx, "Phase: destroy") @@ -179,16 +155,11 @@ func Destroy(ctx context.Context, b *bundle.Bundle, engine engine.EngineType) { var plan *deployplan.Plan if engine.IsDirect() { - // Refuse to destroy when bind blocks point at resources that are currently - // in state: those are pre-existing workspace resources the user imported, - // and destroying them would silently delete data the bundle did not create. - if b.Target != nil && !b.Target.Bind.IsEmpty() { - if err := errIfBoundResourcesInState(&b.DeploymentBundle.StateDB, b.Target.Bind); err != nil { - logdiag.LogError(ctx, err) - return - } - } - + // 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) From cf7794684947c72faded892578dd8a0b6eaf7bf6 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Wed, 22 Jul 2026 14:43:37 +0200 Subject: [PATCH 07/24] Preserve resize semantics for bind_and_update BindAndUpdate hard-coded d.Update, so a bound running cluster with only num_workers/autoscale drift was applied via clusters/edit (which restarts the cluster) instead of the graceful clusters/resize path a normal deploy would take. Dispatch on the planned action instead: Resize for a resize diff, Update otherwise. Recreate/UpdateWithID are already rejected at plan time, so only those two actions can reach here. Add a bind-and-resize acceptance test that binds a running cluster with worker-count-only drift and asserts the resize API is called. Co-authored-by: Isaac --- .../bind/bind-and-resize/databricks.yml.tmpl | 21 ++++++++++ .../deploy/bind/bind-and-resize/out.test.toml | 3 ++ .../deploy/bind/bind-and-resize/output.txt | 38 +++++++++++++++++++ .../bundle/deploy/bind/bind-and-resize/script | 25 ++++++++++++ .../deploy/bind/bind-and-resize/test.toml | 3 ++ bundle/direct/apply.go | 22 ++++++++--- 6 files changed, 106 insertions(+), 6 deletions(-) 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 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..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-resize/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] 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..c6f47d95d00 --- /dev/null +++ b/acceptance/bundle/deploy/bind/bind-and-resize/output.txt @@ -0,0 +1,38 @@ + +>>> [CLI] bundle plan +bind 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... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> 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] + +Deleting files... +Destroy complete! 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/bundle/direct/apply.go b/bundle/direct/apply.go index 8ee6da00ecb..8d3ded20b9e 100644 --- a/bundle/direct/apply.go +++ b/bundle/direct/apply.go @@ -63,13 +63,23 @@ func (d *DeploymentUnit) Bind(ctx context.Context, db *dstate.DeploymentState, i } // BindAndUpdate binds an existing workspace resource and applies the configured -// field changes to it. It defers to Update so the update goes through the same -// DoUpdate retry and WaitAfterUpdate path as a normal update (async resources rely -// on that hook). +// 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 { - err := d.Update(ctx, db, id, newState, planEntry) - if err != nil { - return err + 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) From 205bcb31eb8ec1bb35f7055a7ead6f129d33bd42 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Wed, 22 Jul 2026 14:45:00 +0200 Subject: [PATCH 08/24] Show full bind_and_update action in text plan The text plan printed bind actions via StringShort(), which truncates at the first underscore, so bind_and_update rendered as "bind" and was indistinguishable from a pure bind. Print the full action string for bind actions instead, matching the design's plan example and the JSON output. Co-authored-by: Isaac --- acceptance/bundle/deploy/bind/basic/output.txt | 2 +- acceptance/bundle/deploy/bind/bind-and-resize/output.txt | 2 +- acceptance/bundle/deploy/bind/bind-and-update/output.txt | 2 +- cmd/bundle/plan.go | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/acceptance/bundle/deploy/bind/basic/output.txt b/acceptance/bundle/deploy/bind/basic/output.txt index c1593b7364e..f4a26702252 100644 --- a/acceptance/bundle/deploy/bind/basic/output.txt +++ b/acceptance/bundle/deploy/bind/basic/output.txt @@ -1,6 +1,6 @@ >>> [CLI] bundle plan -bind jobs.foo (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 diff --git a/acceptance/bundle/deploy/bind/bind-and-resize/output.txt b/acceptance/bundle/deploy/bind/bind-and-resize/output.txt index c6f47d95d00..ab3a357c860 100644 --- a/acceptance/bundle/deploy/bind/bind-and-resize/output.txt +++ b/acceptance/bundle/deploy/bind/bind-and-resize/output.txt @@ -1,6 +1,6 @@ >>> [CLI] bundle plan -bind clusters.test_cluster (id: [CLUSTER_ID]) +bind_and_update clusters.test_cluster (id: [CLUSTER_ID]) Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged, 1 to bind diff --git a/acceptance/bundle/deploy/bind/bind-and-update/output.txt b/acceptance/bundle/deploy/bind/bind-and-update/output.txt index 92b414476c2..878130236ce 100644 --- a/acceptance/bundle/deploy/bind/bind-and-update/output.txt +++ b/acceptance/bundle/deploy/bind/bind-and-update/output.txt @@ -1,6 +1,6 @@ >>> [CLI] bundle plan -bind jobs.foo (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 diff --git a/cmd/bundle/plan.go b/cmd/bundle/plan.go index 7d1156efccd..79fb22d4944 100644 --- a/cmd/bundle/plan.go +++ b/cmd/bundle/plan.go @@ -109,8 +109,10 @@ It is useful for previewing changes before running 'bundle deploy'.`, } 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.StringShort(), key, entry.BindID) + fmt.Fprintf(out, "%s %s (id: %s)\n", action.ActionType, key, entry.BindID) } else { fmt.Fprintf(out, "%s %s\n", action.ActionType.StringShort(), key) } From ed089eaacd8cf3198832d505b16a5c4f56b562bf Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Wed, 22 Jul 2026 14:48:15 +0200 Subject: [PATCH 09/24] Keep bind block guarding against recreate after first bind The recreate/update_id guard only fired during the initial bind, when the resource was still absent from state. Once bound, a later config change that forced a recreate took the normal approval flow with no bind-block guard, so a bound resource could be recreated while its bind block was still defined. Extend the guard to the in-state deploy path: while an active bind block references a resource, a planned Recreate/UpdateWithID surfaces the same actionable error as the initial bind. Removing the bind block remains the explicit opt-out. Add a recreate-blocked-in-state acceptance test. Co-authored-by: Isaac --- .../recreate-blocked-in-state/databricks.yml | 18 +++++++++ .../bind/recreate-blocked-in-state/nb.sql | 2 + .../recreate-blocked-in-state/out.test.toml | 3 ++ .../bind/recreate-blocked-in-state/output.txt | 38 +++++++++++++++++++ .../bind/recreate-blocked-in-state/script | 15 ++++++++ bundle/direct/bundle_plan.go | 9 +++++ 6 files changed, 85 insertions(+) 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 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..9cfad3fb0d5 --- /dev/null +++ b/acceptance/bundle/deploy/bind/recreate-blocked-in-state/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = true +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] 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..67450b9b177 --- /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... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> 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/bundle/direct/bundle_plan.go b/bundle/direct/bundle_plan.go index 3821b657ab8..2e75b02021e 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -320,6 +320,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)) From 532eef963b883612db69578a96c73c198294ec14 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:05:24 +0000 Subject: [PATCH 10/24] 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 a6406e86f942b7aa9ffe7ea9ec8311d4d3d5ee9c Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:09:48 +0000 Subject: [PATCH 11/24] 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 38620300061f3f242ff0773368061ba99352fcca Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:20:36 +0000 Subject: [PATCH 12/24] 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 1ee6fe31cc4a967d59ea2ecaec053cb0af8826b5 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:21:33 +0000 Subject: [PATCH 13/24] 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 8bb5cb2241b..4ba0fc3a9a1 100644 --- a/bundle/phases/initialize.go +++ b/bundle/phases/initialize.go @@ -248,8 +248,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 525482c949e3f5686cbe6f2378a3f478a77da653 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:22:00 +0000 Subject: [PATCH 14/24] 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 c120ed9517338e1adbde26585199bfc4e785a7d9 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:22:32 +0000 Subject: [PATCH 15/24] 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 54720c9ee615646da6104eec8c97dc90cb5b26a1 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:23:18 +0000 Subject: [PATCH 16/24] 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 bee72e2b4b8..195ccf1ced2 100644 --- a/cmd/bundle/config_remote_sync.go +++ b/cmd/bundle/config_remote_sync.go @@ -89,6 +89,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 c6d2ef102099ad29e995f803790974bc17f40543 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:24:39 +0000 Subject: [PATCH 17/24] 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 d4548f5576d39bf1f10f2c6539870ca9f1d670f3 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:25:45 +0000 Subject: [PATCH 18/24] 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 0e7122b2fd3..e94cc615ab8 100644 --- a/bundle/internal/schema/annotations.yml +++ b/bundle/internal/schema/annotations.yml @@ -2221,11 +2221,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 024033c8395..cde3a88a615 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -3866,7 +3866,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" } }, @@ -3877,7 +3877,7 @@ }, { "type": "string", - "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" } ] }, @@ -4413,7 +4413,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": { @@ -21171,7 +21171,7 @@ }, { "type": "string", - "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" } ] }, @@ -21236,7 +21236,7 @@ }, { "type": "string", - "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + "pattern": "\\$\\{(var(\\._*\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" } ] } From dd4e9c1d025fabed547958a46d1cfc0c4c55a227 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:26:22 +0000 Subject: [PATCH 19/24] 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 60e4eb174c20be5e17f1b183a0d7a0bf860f38bc Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 13:54:56 +0000 Subject: [PATCH 20/24] 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 e94cc615ab8..26c729583a4 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. @@ -1087,6 +1090,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. @@ -1170,6 +1176,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. @@ -2219,13 +2232,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 8d40e0255f6..38d395d9a29 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,26 +67,32 @@ 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"}, "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"}, @@ -111,6 +121,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"}, @@ -125,14 +136,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"}, @@ -155,6 +169,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"}, @@ -294,6 +309,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"}, @@ -316,6 +332,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"}, @@ -331,6 +348,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"}, @@ -344,12 +362,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"}, @@ -384,31 +404,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"}, @@ -416,44 +444,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 5fcd948f200..11491b00de1 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -356,7 +356,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 e7151b92572..d98a82298e9 100644 --- a/bundle/phases/destroy.go +++ b/bundle/phases/destroy.go @@ -321,12 +321,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 4ba0fc3a9a1..ef74a786ca1 100644 --- a/bundle/phases/initialize.go +++ b/bundle/phases/initialize.go @@ -248,14 +248,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 cde3a88a615..63c13d2a7ad 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": [ { @@ -1631,6 +1652,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" @@ -1679,6 +1704,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" @@ -1701,6 +1730,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" @@ -3860,27 +3893,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": [ { @@ -4412,10 +4424,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" @@ -21161,20 +21169,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": [ { @@ -21221,30 +21215,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 195ccf1ced2..8044a28bf9c 100644 --- a/cmd/bundle/config_remote_sync.go +++ b/cmd/bundle/config_remote_sync.go @@ -89,9 +89,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 ab292e88bf5c21879fa29f9b4453ad1276df1c15 Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 14:36:44 +0000 Subject: [PATCH 21/24] 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 89c7ce5788d8dc2688b5a60894c7ac5da4dd1b3a Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 15:20:21 +0000 Subject: [PATCH 22/24] 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 d202ea6754291bc78e80fe6782834598aa7aaa9f Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 17:04:17 +0000 Subject: [PATCH 23/24] 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 0e9256fa7b7a68d0b5a98e39133c330b6ddbbcfa Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Tue, 3 Nov 2026 17:04:48 +0000 Subject: [PATCH 24/24] 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