From e7fa55c36a1183ce50a49e24453b0dfef0beb5ec Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Tue, 3 Nov 2026 18:51:51 +0100 Subject: [PATCH] Fix 'bundle deployment bind' failing with immutable folders --- .nextchanges/bundles/immutable-bind.md | 1 + .../bind/job/immutable/databricks.yml.tmpl | 26 +++++++++ .../bind/job/immutable/hello_world.py | 1 + .../bind/job/immutable/out.test.toml | 2 + .../deployment/bind/job/immutable/output.txt | 52 +++++++++++++++++ .../deployment/bind/job/immutable/script | 58 +++++++++++++++++++ .../deployment/bind/job/immutable/test.toml | 12 ++++ bundle/phases/bind.go | 12 ++++ 8 files changed, 164 insertions(+) create mode 100644 .nextchanges/bundles/immutable-bind.md create mode 100644 acceptance/bundle/deployment/bind/job/immutable/databricks.yml.tmpl create mode 100644 acceptance/bundle/deployment/bind/job/immutable/hello_world.py create mode 100644 acceptance/bundle/deployment/bind/job/immutable/out.test.toml create mode 100644 acceptance/bundle/deployment/bind/job/immutable/output.txt create mode 100644 acceptance/bundle/deployment/bind/job/immutable/script create mode 100644 acceptance/bundle/deployment/bind/job/immutable/test.toml diff --git a/.nextchanges/bundles/immutable-bind.md b/.nextchanges/bundles/immutable-bind.md new file mode 100644 index 00000000000..826aa8c0832 --- /dev/null +++ b/.nextchanges/bundles/immutable-bind.md @@ -0,0 +1 @@ +* Fix `bundle deployment bind` (and `bundle generate --bind`) failing with `no such node "resources.internal_immutable_snapshots.immutable"` when `workspace.sync_mode` is `immutable`, as reported in [#7306](https://github.com/databricks/cli/issues/7306). diff --git a/acceptance/bundle/deployment/bind/job/immutable/databricks.yml.tmpl b/acceptance/bundle/deployment/bind/job/immutable/databricks.yml.tmpl new file mode 100644 index 00000000000..8e6fded5786 --- /dev/null +++ b/acceptance/bundle/deployment/bind/job/immutable/databricks.yml.tmpl @@ -0,0 +1,26 @@ +bundle: + name: test-bundle-$UNIQUE_NAME + +workspace: + sync_mode: immutable + +resources: + jobs: + foo: + name: test-job-bind-$UNIQUE_NAME + tasks: + - task_key: my_task + environment_key: default + spark_python_task: + python_file: ./hello_world.py + environments: + - environment_key: default + spec: + environment_version: "4" + # References no bundle files. Binding it still plans the whole bundle, so the + # snapshot that the job above depends on has to resolve even here. + schemas: + schema1: + name: $SCHEMA_NAME + catalog_name: main + comment: This schema was created from DABs diff --git a/acceptance/bundle/deployment/bind/job/immutable/hello_world.py b/acceptance/bundle/deployment/bind/job/immutable/hello_world.py new file mode 100644 index 00000000000..7df869a15e7 --- /dev/null +++ b/acceptance/bundle/deployment/bind/job/immutable/hello_world.py @@ -0,0 +1 @@ +print("Hello, World!") diff --git a/acceptance/bundle/deployment/bind/job/immutable/out.test.toml b/acceptance/bundle/deployment/bind/job/immutable/out.test.toml new file mode 100644 index 00000000000..f68b4a89c36 --- /dev/null +++ b/acceptance/bundle/deployment/bind/job/immutable/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = [""] diff --git a/acceptance/bundle/deployment/bind/job/immutable/output.txt b/acceptance/bundle/deployment/bind/job/immutable/output.txt new file mode 100644 index 00000000000..722304ea20c --- /dev/null +++ b/acceptance/bundle/deployment/bind/job/immutable/output.txt @@ -0,0 +1,52 @@ + +=== Create a pre-defined job and schema: +Created job with ID: [JOB_ID] +{ + "full_name": "main.test-schema-[UNIQUE_NAME]", + "catalog_name": "main" +} + +=== Bind the job while the state holds no snapshot yet: +>>> [CLI] bundle deployment bind foo [JOB_ID] --auto-approve +Successfully bound job with an id '[JOB_ID]' +Run 'bundle deploy' to deploy changes to your workspace + +>>> [CLI] bundle plan +create internal_immutable_snapshots.immutable +update jobs.foo +create schemas.schema1 + +Plan: 2 to add, 1 to change, 0 to delete, 0 unchanged + +=== Deploy the job, which deploys the snapshot it depends on: +>>> [CLI] bundle deploy --select jobs.foo +Created internal_immutable_snapshots.immutable +Updated jobs.foo +Files: 0 uploaded, 0 deleted +Resources: 1 created, 1 changed, 0 deleted, 0 unchanged, 1 not selected + +>>> [CLI] jobs get [JOB_ID] +"/Workspace/Users/[UUID]/.snapshots/[SNAPSHOT_HASH]/[SNAPSHOT_HASH]-[UUID]/snapshot/files/hello_world.py" + +=== Bind the schema now that the state holds a snapshot the config does not declare: +>>> [CLI] bundle deployment bind schema1 main.test-schema-[UNIQUE_NAME] --auto-approve +Successfully bound schema with an id 'main.test-schema-[UNIQUE_NAME]' +Run 'bundle deploy' to deploy changes to your workspace + +>>> [CLI] bundle plan +update schemas.schema1 + +Plan: 0 to add, 1 to change, 0 to delete, 2 unchanged + +=== Unbind both and destroy the bundle: +>>> [CLI] bundle deployment unbind foo + +>>> [CLI] bundle deployment unbind schema1 + +>>> [CLI] bundle destroy --auto-approve +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default + +Destroy: 0 deleted + +=== Delete the pre-defined job and schema (both must have survived the destroy):0 +0 diff --git a/acceptance/bundle/deployment/bind/job/immutable/script b/acceptance/bundle/deployment/bind/job/immutable/script new file mode 100644 index 00000000000..77732e334e2 --- /dev/null +++ b/acceptance/bundle/deployment/bind/job/immutable/script @@ -0,0 +1,58 @@ +title "Create a pre-defined job and schema:\n" + +PYTHON_FILE="/Workspace/Users/${CURRENT_USER_NAME}/initial_hello_world.py" + +JOB_ID=$($CLI jobs create --json ' +{ + "name": "test-job-bind-'${UNIQUE_NAME}'", + "tasks": [ + { + "task_key": "my_task", + "environment_key": "default", + "spark_python_task": { + "python_file": "'${PYTHON_FILE}'" + } + } + ], + "environments": [ + { + "environment_key": "default", + "spec": { + "environment_version": "4" + } + } + ] +}' | jq -r '.job_id') +add_repl "$JOB_ID" JOB_ID +echo "Created job with ID: $JOB_ID" + +export SCHEMA_NAME="test-schema-${UNIQUE_NAME}" +$CLI schemas create ${SCHEMA_NAME} main | jq '{full_name, catalog_name}' + +envsubst < databricks.yml.tmpl > databricks.yml + +cleanup() { + title "Delete the pre-defined job and schema (both must have survived the destroy):" + $CLI jobs delete $JOB_ID + echo $? + $CLI schemas delete main.${SCHEMA_NAME} + echo $? +} +trap cleanup EXIT + +title "Bind the job while the state holds no snapshot yet:" +trace $CLI bundle deployment bind foo $JOB_ID --auto-approve +trace $CLI bundle plan + +title "Deploy the job, which deploys the snapshot it depends on:" +trace $CLI bundle deploy --select jobs.foo +trace $CLI jobs get $JOB_ID | jq '.settings.tasks[0].spark_python_task.python_file' + +title "Bind the schema now that the state holds a snapshot the config does not declare:" +trace $CLI bundle deployment bind schema1 main.${SCHEMA_NAME} --auto-approve +trace $CLI bundle plan + +title "Unbind both and destroy the bundle:" +trace $CLI bundle deployment unbind foo +trace $CLI bundle deployment unbind schema1 +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/deployment/bind/job/immutable/test.toml b/acceptance/bundle/deployment/bind/job/immutable/test.toml new file mode 100644 index 00000000000..81ec712df80 --- /dev/null +++ b/acceptance/bundle/deployment/bind/job/immutable/test.toml @@ -0,0 +1,12 @@ +# DMS does not support the immutable snapshot resource yet (see bundle/deploy/immutable/test.toml). +EnvMatrix.DMS = [""] + +Ignore = [ + "databricks.yml", + ".databricks", +] + +[[Repls]] +# Snapshot paths embed the bundle ID and the content hash, both sha256 hex digests. +Old = "[0-9a-f]{64}" +New = "[SNAPSHOT_HASH]" diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index 36c4fcf56b4..8f19f67a7fa 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -11,6 +11,7 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config/engine" "github.com/databricks/cli/bundle/deploy/lock" + "github.com/databricks/cli/bundle/deploy/snapshot" "github.com/databricks/cli/bundle/deployplan" "github.com/databricks/cli/bundle/statemgmt" "github.com/databricks/cli/libs/agent" @@ -47,6 +48,17 @@ func Bind(ctx context.Context, b *bundle.Bundle, opts *BindOptions, stateDesc *s return } + // In immutable sync mode, translated paths reference the snapshot (an internal resource, + // see resources.SnapshotFullPathRef) that only snapshot.PlanUpload registers in the config. + // The plans below cannot resolve those references without it, so register it as plan and + // deploy do. + if b.IsImmutableFolder() { + bundle.ApplyContext(ctx, b, snapshot.PlanUpload(snapshot.PlanUploadOptions{Clean: true})) + if logdiag.HasError(ctx) { + return + } + } + // Direct engine: import into temp state, run plan, check for changes. resourceKey := fmt.Sprintf("resources.%s.%s", opts.Group, opts.ResourceKey) _, statePath := b.StateFilenameDirect(ctx)