From ea8a1ba0d25e365798f8f0acd50659a3a4fffea0 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 9 Nov 2026 07:58:34 +0000 Subject: [PATCH 1/7] Record deployment history for bundle resource unbindings --- .nextchanges/bundles/record-unbind-history.md | 1 + .../bind/feature/out.unbind.false.txt | 2 + .../bind/feature/out.unbind.true.txt | 3 + .../bundle/deployment/bind/feature/output.txt | 2 - .../bundle/deployment/bind/feature/script | 2 +- .../dms/bind-unbind/databricks.yml.tmpl | 3 + acceptance/bundle/dms/bind-unbind/output.txt | 35 +++++--- acceptance/bundle/dms/bind-unbind/script | 8 ++ .../databricks.yml.tmpl | 13 +++ .../dms/unbind-history-failures/out.test.toml | 2 + .../dms/unbind-history-failures/output.txt | 80 +++++++++++++++++++ .../bundle/dms/unbind-history-failures/script | 37 +++++++++ .../dms/unbind-history-failures/test.toml | 1 + bundle/direct/bind.go | 37 +++++++-- bundle/direct/bind_test.go | 58 ++++++++++++++ bundle/phases/bind.go | 33 +++++++- 16 files changed, 294 insertions(+), 23 deletions(-) create mode 100644 .nextchanges/bundles/record-unbind-history.md create mode 100644 acceptance/bundle/deployment/bind/feature/out.unbind.false.txt create mode 100644 acceptance/bundle/deployment/bind/feature/out.unbind.true.txt create mode 100644 acceptance/bundle/dms/unbind-history-failures/databricks.yml.tmpl create mode 100644 acceptance/bundle/dms/unbind-history-failures/out.test.toml create mode 100644 acceptance/bundle/dms/unbind-history-failures/output.txt create mode 100644 acceptance/bundle/dms/unbind-history-failures/script create mode 100644 acceptance/bundle/dms/unbind-history-failures/test.toml create mode 100644 bundle/direct/bind_test.go diff --git a/.nextchanges/bundles/record-unbind-history.md b/.nextchanges/bundles/record-unbind-history.md new file mode 100644 index 00000000000..f615c93c5a3 --- /dev/null +++ b/.nextchanges/bundles/record-unbind-history.md @@ -0,0 +1 @@ +* Record deployment history when unbinding resources with `bundle deployment unbind`. ([#7401](https://github.com/databricks/cli/pull/7401)) diff --git a/acceptance/bundle/deployment/bind/feature/out.unbind.false.txt b/acceptance/bundle/deployment/bind/feature/out.unbind.false.txt new file mode 100644 index 00000000000..6779f487c1f --- /dev/null +++ b/acceptance/bundle/deployment/bind/feature/out.unbind.false.txt @@ -0,0 +1,2 @@ + +>>> [CLI] bundle deployment unbind feature1 diff --git a/acceptance/bundle/deployment/bind/feature/out.unbind.true.txt b/acceptance/bundle/deployment/bind/feature/out.unbind.true.txt new file mode 100644 index 00000000000..2dace068795 --- /dev/null +++ b/acceptance/bundle/deployment/bind/feature/out.unbind.true.txt @@ -0,0 +1,3 @@ + +>>> [CLI] bundle deployment unbind feature1 +Warn: Failed to record unbind history: failed to create deployment version: operations.resource_key must have a known resource type prefix (e.g. 'jobs.', 'pipelines.'): features.feature1 diff --git a/acceptance/bundle/deployment/bind/feature/output.txt b/acceptance/bundle/deployment/bind/feature/output.txt index d4d47f6a660..e0bca28a059 100644 --- a/acceptance/bundle/deployment/bind/feature/output.txt +++ b/acceptance/bundle/deployment/bind/feature/output.txt @@ -15,8 +15,6 @@ Resources: Name: main.myschema.boundfeature URL: [DATABRICKS_URL]/explore/data/features/main/myschema/boundfeature?w=[WORKSPACE_ID] ->>> [CLI] bundle deployment unbind feature1 - >>> [CLI] bundle summary Name: test-bundle Target: default diff --git a/acceptance/bundle/deployment/bind/feature/script b/acceptance/bundle/deployment/bind/feature/script index 8e27f69056c..019816f7499 100644 --- a/acceptance/bundle/deployment/bind/feature/script +++ b/acceptance/bundle/deployment/bind/feature/script @@ -1,5 +1,5 @@ trace $CLI bundle deployment bind feature1 main.myschema.boundfeature --auto-approve trace $CLI bundle summary -trace $CLI bundle deployment unbind feature1 +trace $CLI bundle deployment unbind feature1 2> out.unbind.$DMS.txt trace $CLI bundle summary diff --git a/acceptance/bundle/dms/bind-unbind/databricks.yml.tmpl b/acceptance/bundle/dms/bind-unbind/databricks.yml.tmpl index 92a70710626..417b10b4962 100644 --- a/acceptance/bundle/dms/bind-unbind/databricks.yml.tmpl +++ b/acceptance/bundle/dms/bind-unbind/databricks.yml.tmpl @@ -4,3 +4,6 @@ resources: jobs: foo: name: foo + permissions: + - group_name: users + level: CAN_VIEW diff --git a/acceptance/bundle/dms/bind-unbind/output.txt b/acceptance/bundle/dms/bind-unbind/output.txt index 3c0d02308bf..3afd7f718d3 100644 --- a/acceptance/bundle/dms/bind-unbind/output.txt +++ b/acceptance/bundle/dms/bind-unbind/output.txt @@ -1,6 +1,5 @@ === Bind an existing job with deployment history enabled - >>> musterr [CLI] bundle deployment bind foo [JOB_ID] Plan: update resources.jobs.foo @@ -20,7 +19,7 @@ Successfully bound job with an id '[JOB_ID]' Run 'bundle deploy' to deploy changes to your workspace >>> print_requests.py --dms //versions --oneline -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "3"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UPDATE", "resource_key": "jobs.foo"}], "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "3"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_BIND", "resource_key": "jobs.foo"}], "version_type": "VERSION_TYPE_DEPLOY"}} {"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/3/operations/jobs.foo", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[JOB_ID]", "sequence_id": 0, "state": "{\"state\":{\"deployment\":{\"kind\":\"BUNDLE\",\"metadata_file_path\":\"/Workspace/Users/[USERNAME]/.bundle/dms-bind-unbind-[UNIQUE_NAME]/default/state/metadata.json\"},\"edit_mode\":\"UI_LOCKED\",\"format\":\"MULTI_TASK\",\"max_concurrent_runs\":1,\"name\":\"foo\",\"queue\":{\"enabled\":true}}}", "status": "OPERATION_STATUS_SUCCEEDED"}} {"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/3/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} @@ -33,19 +32,19 @@ Run 'bundle deploy' to deploy changes to your workspace >>> MSYS_NO_PATHCONV=1 [CLI] bundle-deployments list-resources deployments/[DEPLOYMENT_ID] -o json [ { - "last_action_type": "OPERATION_ACTION_TYPE_UPDATE", + "last_action_type": "OPERATION_ACTION_TYPE_BIND", "resource_id": "[JOB_ID]", "resource_key": "jobs.foo" } ] === Deploy keeps the bound job ID with recorded history - >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dms-bind-unbind-[UNIQUE_NAME]/default/files... +Created jobs.foo.permissions Updated jobs.foo Files: 2 uploaded, 0 deleted -Resources: 0 created, 1 changed, 0 deleted, 0 unchanged +Resources: 1 created, 1 changed, 0 deleted, 0 unchanged >>> jq {features, resource_id: .state["resources.jobs.foo"].__id__} .databricks/bundle/default/resources.json { @@ -56,9 +55,16 @@ Resources: 0 created, 1 changed, 0 deleted, 0 unchanged } === Unbind preserves the shadow history marker and leaves the job intact - >>> [CLI] bundle deployment unbind foo +>>> print_requests.py --dms //versions --oneline --keep +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "6"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.foo"}, {"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.foo.permissions"}], "previous_version_id": "4", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/6/operations/jobs.foo", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} +{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/6/operations/jobs.foo.permissions", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/6/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} + +>>> print_requests.py //jobs //permissions + >>> jq {features, state} .databricks/bundle/default/resources.json { "features": { @@ -72,8 +78,12 @@ Resources: 0 created, 1 changed, 0 deleted, 0 unchanged "job_id": [JOB_ID] } -=== Rebind preserves the existing shadow history marker +=== Unbinding an already unbound resource creates no history version +>>> [CLI] bundle deployment unbind foo + +>>> print_requests.py --dms //versions +=== Rebind preserves the existing shadow history marker >>> [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 @@ -87,16 +97,16 @@ Run 'bundle deploy' to deploy changes to your workspace } >>> print_requests.py --dms //versions --oneline -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "7"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UPDATE", "resource_key": "jobs.foo"}], "previous_version_id": "4", "version_type": "VERSION_TYPE_DEPLOY"}} -{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/7/operations/jobs.foo", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[JOB_ID]", "sequence_id": 0, "state": "{\"state\":{\"deployment\":{\"kind\":\"BUNDLE\",\"metadata_file_path\":\"/Workspace/Users/[USERNAME]/.bundle/dms-bind-unbind-[UNIQUE_NAME]/default/state/metadata.json\"},\"edit_mode\":\"UI_LOCKED\",\"format\":\"MULTI_TASK\",\"max_concurrent_runs\":1,\"name\":\"foo\",\"queue\":{\"enabled\":true}}}", "status": "OPERATION_STATUS_SUCCEEDED"}} -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/7/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "10"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_BIND", "resource_key": "jobs.foo"}], "previous_version_id": "6", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/10/operations/jobs.foo", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[JOB_ID]", "sequence_id": 0, "state": "{\"state\":{\"deployment\":{\"kind\":\"BUNDLE\",\"metadata_file_path\":\"/Workspace/Users/[USERNAME]/.bundle/dms-bind-unbind-[UNIQUE_NAME]/default/state/metadata.json\"},\"edit_mode\":\"UI_LOCKED\",\"format\":\"MULTI_TASK\",\"max_concurrent_runs\":1,\"name\":\"foo\",\"queue\":{\"enabled\":true}}}", "status": "OPERATION_STATUS_SUCCEEDED"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/10/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} === A subsequent deploy continues managing the same job - >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dms-bind-unbind-[UNIQUE_NAME]/default/files... +Created jobs.foo.permissions Files: 0 uploaded, 0 deleted -Resources: 0 created, 0 changed, 0 deleted, 1 unchanged +Resources: 1 created, 0 changed, 0 deleted, 1 unchanged >>> jq {features, resource_id: .state["resources.jobs.foo"].__id__} .databricks/bundle/default/resources.json { @@ -107,7 +117,6 @@ Resources: 0 created, 0 changed, 0 deleted, 1 unchanged } === A rejected duplicate bind creates no history version - >>> musterr [CLI] bundle deployment bind foo [JOB_ID] --auto-approve Error: Resource already managed diff --git a/acceptance/bundle/dms/bind-unbind/script b/acceptance/bundle/dms/bind-unbind/script index 8c7d75c3d43..8d0f34a0b11 100644 --- a/acceptance/bundle/dms/bind-unbind/script +++ b/acceptance/bundle/dms/bind-unbind/script @@ -23,13 +23,21 @@ title "Deploy keeps the bound job ID with recorded history" trace $CLI bundle deploy trace jq '{features, resource_id: .state["resources.jobs.foo"].__id__}' .databricks/bundle/default/resources.json +rm -f "$OUT_REQUESTS" + title "Unbind preserves the shadow history marker and leaves the job intact" trace $CLI bundle deployment unbind foo +trace print_requests.py --dms //versions --oneline --keep +trace print_requests.py //jobs //permissions trace jq '{features, state}' .databricks/bundle/default/resources.json trace $CLI jobs get "$job_id" | jq '{job_id}' rm -f "$OUT_REQUESTS" +title "Unbinding an already unbound resource creates no history version" +trace $CLI bundle deployment unbind foo +trace print_requests.py --dms //versions + title "Rebind preserves the existing shadow history marker" trace $CLI bundle deployment bind foo "$job_id" --auto-approve trace jq '{features, resource_id: .state["resources.jobs.foo"].__id__}' .databricks/bundle/default/resources.json diff --git a/acceptance/bundle/dms/unbind-history-failures/databricks.yml.tmpl b/acceptance/bundle/dms/unbind-history-failures/databricks.yml.tmpl new file mode 100644 index 00000000000..a30c578aa55 --- /dev/null +++ b/acceptance/bundle/dms/unbind-history-failures/databricks.yml.tmpl @@ -0,0 +1,13 @@ +bundle: + name: dms-unbind-history-failures-$UNIQUE_NAME + +resources: + jobs: + create_failed: + name: create_failed + record_failed: + name: record_failed + complete_failed: + name: complete_failed + upload_failed: + name: upload_failed diff --git a/acceptance/bundle/dms/unbind-history-failures/out.test.toml b/acceptance/bundle/dms/unbind-history-failures/out.test.toml new file mode 100644 index 00000000000..88e6fbd847c --- /dev/null +++ b/acceptance/bundle/dms/unbind-history-failures/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DMS = ["true"] diff --git a/acceptance/bundle/dms/unbind-history-failures/output.txt b/acceptance/bundle/dms/unbind-history-failures/output.txt new file mode 100644 index 00000000000..782f59dd225 --- /dev/null +++ b/acceptance/bundle/dms/unbind-history-failures/output.txt @@ -0,0 +1,80 @@ + +=== A version creation failure does not prevent unbinding +>>> fault.py POST /api/2.0/bundle/deployments/*/versions 400 0 1 + +>>> [CLI] bundle deployment unbind create_failed +Warn: Failed to record unbind history: failed to create deployment version: Fault injected by test. + +=== An operation recording failure does not prevent unbinding +>>> fault.py PATCH /api/2.0/bundle/** 400 0 1 + +>>> [CLI] bundle deployment unbind record_failed +Warn: Failed to record deployment history: recording operation for resources.jobs.record_failed: Fault injected by test. + +=== A version completion failure does not prevent unbinding +>>> fault.py POST /api/2.0/bundle/deployments/*/versions/*/complete 400 0 1 + +>>> [CLI] bundle deployment unbind complete_failed +Warn: Failed to complete deployment history version: Fault injected by test. + +=== Successful unbindings survived the remote state round trips and the job still exists +>>> jq .state .databricks/bundle/default/resources.json +{ + "resources.jobs.upload_failed": { + "__id__": "[JOB_ID]", + "state": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/dms-unbind-history-failures-[UNIQUE_NAME]/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "upload_failed", + "queue": { + "enabled": true + } + } + } +} + +>>> [CLI] jobs get [JOB_ID] +{ + "job_id": [JOB_ID], + "name": "unbind-target-[UNIQUE_NAME]" +} + +>>> print_requests.py --dms //versions --oneline +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "14"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.create_failed"}], "previous_version_id": "12", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "15"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.record_failed"}], "previous_version_id": "12", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/15/operations/jobs.record_failed", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/15/complete", "body": {"completion_reason": "VERSION_COMPLETE_FAILURE"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "17"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.complete_failed"}], "previous_version_id": "15", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/17/operations/jobs.complete_failed", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/17/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} + +=== A remote state upload failure fails the unbind version +>>> fault.py POST /api/2.0/workspace-files/import-file/**resources.json 400 0 1 + +>>> musterr [CLI] bundle deployment unbind upload_failed +Error: Fault injected by test. (400 INJECTED) + +Endpoint: POST [DATABRICKS_URL]/api/2.0/workspace-files/import-file/Workspace%2FUsers%2F[USERNAME]%2F.bundle%2Fdms-unbind-history-failures-[UNIQUE_NAME]%2Fdefault%2Fstate%2Fresources.json?overwrite=true +HTTP Status: 400 Bad Request +API error_code: INJECTED +API message: Fault injected by test. + + +>>> print_requests.py --dms //versions --oneline +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "19"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.upload_failed"}], "previous_version_id": "17", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/19/operations/jobs.upload_failed", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/19/complete", "body": {"completion_reason": "VERSION_COMPLETE_FAILURE"}} + +=== Remote state still contains the binding whose upload failed +>>> [CLI] bundle summary -o json +{ + "complete_failed": null, + "create_failed": null, + "record_failed": null, + "upload_failed": "[JOB_ID]" +} diff --git a/acceptance/bundle/dms/unbind-history-failures/script b/acceptance/bundle/dms/unbind-history-failures/script new file mode 100644 index 00000000000..ef0101567b2 --- /dev/null +++ b/acceptance/bundle/dms/unbind-history-failures/script @@ -0,0 +1,37 @@ +envsubst < databricks.yml.tmpl > databricks.yml +job_id=$($CLI jobs create --json '{"name": "unbind-target-'$UNIQUE_NAME'"}' | jq -r '.job_id') +add_repl "$job_id" JOB_ID +for key in create_failed record_failed complete_failed upload_failed; do + $CLI bundle deployment bind "$key" "$job_id" --auto-approve >> LOG.setup 2>&1 +done +rm -f "$OUT_REQUESTS" + +title "A version creation failure does not prevent unbinding" +trace fault.py "POST /api/2.0/bundle/deployments/*/versions" 400 0 1 +trace $CLI bundle deployment unbind create_failed +rm -rf .databricks + +title "An operation recording failure does not prevent unbinding" +trace fault.py "PATCH /api/2.0/bundle/**" 400 0 1 +trace $CLI bundle deployment unbind record_failed +rm -rf .databricks + +title "A version completion failure does not prevent unbinding" +trace fault.py "POST /api/2.0/bundle/deployments/*/versions/*/complete" 400 0 1 +trace $CLI bundle deployment unbind complete_failed + +title "Successful unbindings survived the remote state round trips and the job still exists" +trace jq '.state' .databricks/bundle/default/resources.json +trace $CLI jobs get "$job_id" | jq '{job_id, name: .settings.name}' +trace print_requests.py --dms //versions --oneline + + +title "A remote state upload failure fails the unbind version" +trace fault.py "POST /api/2.0/workspace-files/import-file/**resources.json" 400 0 1 +trace musterr $CLI bundle deployment unbind upload_failed +trace print_requests.py --dms //versions --oneline + +rm -rf .databricks +title "Remote state still contains the binding whose upload failed" +trace $CLI bundle summary -o json | jq '.resources.jobs | map_values(.id // null)' +rm -f "$OUT_REQUESTS" diff --git a/acceptance/bundle/dms/unbind-history-failures/test.toml b/acceptance/bundle/dms/unbind-history-failures/test.toml new file mode 100644 index 00000000000..0e4c9337799 --- /dev/null +++ b/acceptance/bundle/dms/unbind-history-failures/test.toml @@ -0,0 +1 @@ +Cloud = false # Inject failures into the deployment history service. diff --git a/bundle/direct/bind.go b/bundle/direct/bind.go index 289b1076b29..11eb6cc433b 100644 --- a/bundle/direct/bind.go +++ b/bundle/direct/bind.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "slices" "strings" "github.com/databricks/cli/bundle/config" @@ -42,6 +43,12 @@ type DMSBindMetadata struct { ResourceState dstate.ResourceEntry } +// DMSUnbindMetadata holds the removed resource keys and serial to publish after unbind is committed. +type DMSUnbindMetadata struct { + Serial int + ResourceKeys []string +} + // BindResult contains the result of a bind operation including any detected changes. type BindResult struct { // DMSMetadata is nil when deployment history is disabled. @@ -262,18 +269,30 @@ func (result *BindResult) Cancel() { // Unbind removes a resource from direct engine state without deleting // the workspace resource. Also removes associated permissions/grants entries. -func (b *DeploymentBundle) Unbind(ctx context.Context, statePath, resourceKey string) error { +func (b *DeploymentBundle) Unbind(ctx context.Context, configRoot *config.Root, statePath, resourceKey string) (*DMSUnbindMetadata, error) { if err := b.StateDB.Open(ctx, statePath, dstate.ReadingFile, dstate.WithRecovery(true), dstate.OpenDmsArgs{}); err != nil { - return err + return nil, err } if err := b.StateDB.UpgradeToWrite(); err != nil { - return err + return nil, err + } + + var metadata *DMSUnbindMetadata + if b.dmsMetadataEnabled(ctx, configRoot) { + metadata = &DMSUnbindMetadata{Serial: b.StateDB.Data.Serial} + // Finalize commits the removals below as one WAL transaction. + if b.StateDB.Data.State != nil { + metadata.Serial++ + } + if b.StateDB.GetResourceID(resourceKey) != "" { + metadata.ResourceKeys = append(metadata.ResourceKeys, resourceKey) + } } // Delete the main resource err := b.StateDB.DeleteState(ctx, resourceKey, false) if err != nil { - return err + return nil, err } log.Infof(ctx, "Unbound %s", resourceKey) @@ -287,12 +306,18 @@ func (b *DeploymentBundle) Unbind(ctx context.Context, statePath, resourceKey st if key == permissionsKey || key == grantsKey || strings.HasPrefix(key, resourceKey+".") { err = b.StateDB.DeleteState(ctx, key, false) if err != nil { - return err + return nil, err + } + if metadata != nil { + metadata.ResourceKeys = append(metadata.ResourceKeys, key) } log.Infof(ctx, "Unbound %s", key) } } + if metadata != nil { + slices.Sort(metadata.ResourceKeys) + } _, err = b.StateDB.Finalize(ctx) - return err + return metadata, err } diff --git a/bundle/direct/bind_test.go b/bundle/direct/bind_test.go new file mode 100644 index 00000000000..7bc78a2e5e2 --- /dev/null +++ b/bundle/direct/bind_test.go @@ -0,0 +1,58 @@ +package direct_test + +import ( + "path/filepath" + "testing" + + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/direct" + "github.com/databricks/cli/bundle/direct/dstate" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnbindReturnsRemovedKeys(t *testing.T) { + for _, tc := range []struct { + name string + keys []string + history bool + }{ + {"resource", []string{"resources.jobs.foo"}, true}, + {"children", []string{"resources.jobs.foo", "resources.jobs.foo.permissions", "resources.jobs.foo.grants"}, true}, + {"children_without_parent", []string{"resources.jobs.foo.permissions"}, true}, + {"missing", nil, true}, + {"history_disabled", []string{"resources.jobs.foo"}, false}, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := t.Context() + statePath := filepath.Join(t.TempDir(), "resources.json") + var b direct.DeploymentBundle + require.NoError(t, b.StateDB.Open(ctx, statePath, dstate.ReadingFile, false, dstate.OpenDmsArgs{})) + require.NoError(t, b.StateDB.UpgradeToWrite()) + for _, key := range append(tc.keys, "resources.jobs.foobar") { + require.NoError(t, b.StateDB.SaveState(ctx, key, "id", map[string]string{"name": key}, nil)) + } + _, err := b.StateDB.Finalize(ctx) + require.NoError(t, err) + + root := &config.Root{Experimental: &config.Experimental{DeploymentHistory: &tc.history}} + metadata, err := b.Unbind(ctx, root, statePath, "resources.jobs.foo") + require.NoError(t, err) + if tc.history { + require.NotNil(t, metadata) + assert.ElementsMatch(t, tc.keys, metadata.ResourceKeys) + } else { + assert.Nil(t, metadata) + } + + require.NoError(t, b.StateDB.Open(ctx, statePath, dstate.ReadingFile, false, dstate.OpenDmsArgs{})) + assert.Len(t, b.StateDB.Data.State, 1) + if metadata != nil { + assert.Equal(t, b.StateDB.Data.Serial, metadata.Serial) + } + assert.Equal(t, "id", b.StateDB.GetResourceID("resources.jobs.foobar")) + _, err = b.StateDB.Finalize(ctx) + require.NoError(t, err) + }) + } +} diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index fb9a1f316be..8b6ba31b90a 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -197,7 +197,10 @@ func Unbind(ctx context.Context, b *bundle.Bundle, group, resourceKey string, en return } + var historyVersion string + var historyErr error defer func() { + completeBindingHistory(ctx, b, historyVersion, historyErr == nil && !logdiag.HasError(ctx)) bundle.ApplyContext(ctx, b, lock.Release(lock.GoalUnbind)) }() @@ -210,12 +213,17 @@ func Unbind(ctx context.Context, b *bundle.Bundle, group, resourceKey string, en fullResourceKey := fmt.Sprintf("resources.%s.%s", group, resourceKey) _, statePath := b.StateFilenameDirect(ctx) - err := b.DeploymentBundle.Unbind(ctx, statePath, fullResourceKey) + metadata, err := b.DeploymentBundle.Unbind(ctx, &b.Config, statePath, fullResourceKey) if err != nil { logdiag.LogError(ctx, err) return } + historyVersion, historyErr = recordUnbindHistory(ctx, b, metadata) + if historyErr != nil { + log.Warnf(ctx, "Failed to record unbind history: %s", historyErr) + } + statemgmt.PushResourcesState(ctx, b) } @@ -240,6 +248,29 @@ func recordBindHistory(ctx context.Context, b *bundle.Bundle, resourceKey string return history, history.buffer.Drain(ctx) } +// recordUnbindHistory publishes committed removals when DMS metadata was requested. +func recordUnbindHistory(ctx context.Context, b *bundle.Bundle, metadata *direct.DMSUnbindMetadata) (string, error) { + if metadata == nil || len(metadata.ResourceKeys) == 0 { + return "", nil + } + staged := make([]bundledeployments.StagedOperation, 0, len(metadata.ResourceKeys)) + for _, key := range metadata.ResourceKeys { + staged = append(staged, bundledeployments.StagedOperation{ + ResourceKey: strings.TrimPrefix(key, dms.StatePrefix), + ActionType: bundledeployments.OperationActionTypeOperationActionTypeUnbind, + }) + } + versionName, err := createBindingVersion(ctx, b, metadata.Serial, staged) + if err != nil { + return "", err + } + var recordErr error + for _, key := range metadata.ResourceKeys { + recordErr = errors.Join(recordErr, recordBindingOperation(ctx, b, versionName, key, nil)) + } + return versionName, recordErr +} + // createBindingVersion claims a history version for the staged binding operations. func createBindingVersion(ctx context.Context, b *bundle.Bundle, serial int, staged []bundledeployments.StagedOperation) (*bindingHistory, error) { deploymentID, current, err := dms.LookupDeployment(ctx, b.WorkspaceClient(ctx), b.Config.Workspace.StatePath) From 35eaa8efddf12b1411dc64811cc09b1def221297 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 9 Nov 2026 09:21:24 +0000 Subject: [PATCH 2/7] Address unbind history self-review findings --- .nextchanges/bundles/record-unbind-history.md | 2 +- .../feature/{out.unbind.false.txt => out.unbind.plain.txt} | 0 .../{out.unbind.true.txt => out.unbind.recorded.txt} | 0 acceptance/bundle/deployment/bind/feature/script | 2 +- acceptance/bundle/dms/bind-unbind/output.txt | 7 ++++--- acceptance/bundle/dms/bind-unbind/script | 5 +++-- bundle/direct/bind.go | 1 + 7 files changed, 10 insertions(+), 7 deletions(-) rename acceptance/bundle/deployment/bind/feature/{out.unbind.false.txt => out.unbind.plain.txt} (100%) rename acceptance/bundle/deployment/bind/feature/{out.unbind.true.txt => out.unbind.recorded.txt} (100%) diff --git a/.nextchanges/bundles/record-unbind-history.md b/.nextchanges/bundles/record-unbind-history.md index f615c93c5a3..dc603090da6 100644 --- a/.nextchanges/bundles/record-unbind-history.md +++ b/.nextchanges/bundles/record-unbind-history.md @@ -1 +1 @@ -* Record deployment history when unbinding resources with `bundle deployment unbind`. ([#7401](https://github.com/databricks/cli/pull/7401)) +* Record deployment history when unbinding resources with `bundle deployment unbind` when deployment history is enabled. ([#7401](https://github.com/databricks/cli/pull/7401)) diff --git a/acceptance/bundle/deployment/bind/feature/out.unbind.false.txt b/acceptance/bundle/deployment/bind/feature/out.unbind.plain.txt similarity index 100% rename from acceptance/bundle/deployment/bind/feature/out.unbind.false.txt rename to acceptance/bundle/deployment/bind/feature/out.unbind.plain.txt diff --git a/acceptance/bundle/deployment/bind/feature/out.unbind.true.txt b/acceptance/bundle/deployment/bind/feature/out.unbind.recorded.txt similarity index 100% rename from acceptance/bundle/deployment/bind/feature/out.unbind.true.txt rename to acceptance/bundle/deployment/bind/feature/out.unbind.recorded.txt diff --git a/acceptance/bundle/deployment/bind/feature/script b/acceptance/bundle/deployment/bind/feature/script index 019816f7499..7cb00fef219 100644 --- a/acceptance/bundle/deployment/bind/feature/script +++ b/acceptance/bundle/deployment/bind/feature/script @@ -1,5 +1,5 @@ trace $CLI bundle deployment bind feature1 main.myschema.boundfeature --auto-approve trace $CLI bundle summary -trace $CLI bundle deployment unbind feature1 2> out.unbind.$DMS.txt +trace $CLI bundle deployment unbind feature1 2> out.unbind.$OUTPUT_MODE.txt trace $CLI bundle summary diff --git a/acceptance/bundle/dms/bind-unbind/output.txt b/acceptance/bundle/dms/bind-unbind/output.txt index 3afd7f718d3..ba3c7688c96 100644 --- a/acceptance/bundle/dms/bind-unbind/output.txt +++ b/acceptance/bundle/dms/bind-unbind/output.txt @@ -57,14 +57,12 @@ Resources: 1 created, 1 changed, 0 deleted, 0 unchanged === Unbind preserves the shadow history marker and leaves the job intact >>> [CLI] bundle deployment unbind foo ->>> print_requests.py --dms //versions --oneline --keep +>>> print_requests.py --dms //versions //jobs //permissions --oneline {"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "6"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.foo"}, {"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.foo.permissions"}], "previous_version_id": "4", "version_type": "VERSION_TYPE_DEPLOY"}} {"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/6/operations/jobs.foo", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} {"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/6/operations/jobs.foo.permissions", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} {"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/6/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} ->>> print_requests.py //jobs //permissions - >>> jq {features, state} .databricks/bundle/default/resources.json { "features": { @@ -78,6 +76,9 @@ Resources: 1 created, 1 changed, 0 deleted, 0 unchanged "job_id": [JOB_ID] } +>>> MSYS_NO_PATHCONV=1 [CLI] bundle-deployments list-resources deployments/[DEPLOYMENT_ID] -o json +[] + === Unbinding an already unbound resource creates no history version >>> [CLI] bundle deployment unbind foo diff --git a/acceptance/bundle/dms/bind-unbind/script b/acceptance/bundle/dms/bind-unbind/script index 8d0f34a0b11..1d973b8fe05 100644 --- a/acceptance/bundle/dms/bind-unbind/script +++ b/acceptance/bundle/dms/bind-unbind/script @@ -27,10 +27,11 @@ rm -f "$OUT_REQUESTS" title "Unbind preserves the shadow history marker and leaves the job intact" trace $CLI bundle deployment unbind foo -trace print_requests.py --dms //versions --oneline --keep -trace print_requests.py //jobs //permissions +trace print_requests.py --dms //versions //jobs //permissions --oneline trace jq '{features, state}' .databricks/bundle/default/resources.json trace $CLI jobs get "$job_id" | jq '{job_id}' +trace MSYS_NO_PATHCONV=1 $CLI bundle-deployments list-resources "deployments/${deployment_id}" -o json | jq -S 'map({resource_key, resource_id, last_action_type})' + rm -f "$OUT_REQUESTS" diff --git a/bundle/direct/bind.go b/bundle/direct/bind.go index 11eb6cc433b..3958c5bc99a 100644 --- a/bundle/direct/bind.go +++ b/bundle/direct/bind.go @@ -269,6 +269,7 @@ func (result *BindResult) Cancel() { // Unbind removes a resource from direct engine state without deleting // the workspace resource. Also removes associated permissions/grants entries. +// It returns nil DMS metadata when deployment history is disabled. func (b *DeploymentBundle) Unbind(ctx context.Context, configRoot *config.Root, statePath, resourceKey string) (*DMSUnbindMetadata, error) { if err := b.StateDB.Open(ctx, statePath, dstate.ReadingFile, dstate.WithRecovery(true), dstate.OpenDmsArgs{}); err != nil { return nil, err From d14425054fd2ee38980054f1be568e31fd5e3e35 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 9 Nov 2026 13:01:06 +0000 Subject: [PATCH 3/7] Update unbind history expectations without duplicate state writes --- acceptance/bundle/dms/bind-unbind/output.txt | 14 ++++++------- .../dms/unbind-history-failures/output.txt | 20 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/acceptance/bundle/dms/bind-unbind/output.txt b/acceptance/bundle/dms/bind-unbind/output.txt index ba3c7688c96..84bd2d98082 100644 --- a/acceptance/bundle/dms/bind-unbind/output.txt +++ b/acceptance/bundle/dms/bind-unbind/output.txt @@ -58,10 +58,10 @@ Resources: 1 created, 1 changed, 0 deleted, 0 unchanged >>> [CLI] bundle deployment unbind foo >>> print_requests.py --dms //versions //jobs //permissions --oneline -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "6"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.foo"}, {"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.foo.permissions"}], "previous_version_id": "4", "version_type": "VERSION_TYPE_DEPLOY"}} -{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/6/operations/jobs.foo", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} -{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/6/operations/jobs.foo.permissions", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/6/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "5"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.foo"}, {"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.foo.permissions"}], "previous_version_id": "4", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/5/operations/jobs.foo", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} +{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/5/operations/jobs.foo.permissions", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/5/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} >>> jq {features, state} .databricks/bundle/default/resources.json { @@ -98,9 +98,9 @@ Run 'bundle deploy' to deploy changes to your workspace } >>> print_requests.py --dms //versions --oneline -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "10"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_BIND", "resource_key": "jobs.foo"}], "previous_version_id": "6", "version_type": "VERSION_TYPE_DEPLOY"}} -{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/10/operations/jobs.foo", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[JOB_ID]", "sequence_id": 0, "state": "{\"state\":{\"deployment\":{\"kind\":\"BUNDLE\",\"metadata_file_path\":\"/Workspace/Users/[USERNAME]/.bundle/dms-bind-unbind-[UNIQUE_NAME]/default/state/metadata.json\"},\"edit_mode\":\"UI_LOCKED\",\"format\":\"MULTI_TASK\",\"max_concurrent_runs\":1,\"name\":\"foo\",\"queue\":{\"enabled\":true}}}", "status": "OPERATION_STATUS_SUCCEEDED"}} -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/10/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "8"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_BIND", "resource_key": "jobs.foo"}], "previous_version_id": "5", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/8/operations/jobs.foo", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[JOB_ID]", "sequence_id": 0, "state": "{\"state\":{\"deployment\":{\"kind\":\"BUNDLE\",\"metadata_file_path\":\"/Workspace/Users/[USERNAME]/.bundle/dms-bind-unbind-[UNIQUE_NAME]/default/state/metadata.json\"},\"edit_mode\":\"UI_LOCKED\",\"format\":\"MULTI_TASK\",\"max_concurrent_runs\":1,\"name\":\"foo\",\"queue\":{\"enabled\":true}}}", "status": "OPERATION_STATUS_SUCCEEDED"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/8/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} === A subsequent deploy continues managing the same job >>> [CLI] bundle deploy diff --git a/acceptance/bundle/dms/unbind-history-failures/output.txt b/acceptance/bundle/dms/unbind-history-failures/output.txt index 782f59dd225..b7219857f28 100644 --- a/acceptance/bundle/dms/unbind-history-failures/output.txt +++ b/acceptance/bundle/dms/unbind-history-failures/output.txt @@ -45,13 +45,13 @@ Warn: Failed to complete deployment history version: Fault injected by test. } >>> print_requests.py --dms //versions --oneline -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "14"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.create_failed"}], "previous_version_id": "12", "version_type": "VERSION_TYPE_DEPLOY"}} -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "15"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.record_failed"}], "previous_version_id": "12", "version_type": "VERSION_TYPE_DEPLOY"}} -{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/15/operations/jobs.record_failed", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/15/complete", "body": {"completion_reason": "VERSION_COMPLETE_FAILURE"}} -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "17"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.complete_failed"}], "previous_version_id": "15", "version_type": "VERSION_TYPE_DEPLOY"}} -{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/17/operations/jobs.complete_failed", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/17/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "10"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.create_failed"}], "previous_version_id": "9", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "11"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.record_failed"}], "previous_version_id": "9", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/11/operations/jobs.record_failed", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/11/complete", "body": {"completion_reason": "VERSION_COMPLETE_FAILURE"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "12"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.complete_failed"}], "previous_version_id": "11", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/12/operations/jobs.complete_failed", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/12/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} === A remote state upload failure fails the unbind version >>> fault.py POST /api/2.0/workspace-files/import-file/**resources.json 400 0 1 @@ -66,9 +66,9 @@ API message: Fault injected by test. >>> print_requests.py --dms //versions --oneline -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "19"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.upload_failed"}], "previous_version_id": "17", "version_type": "VERSION_TYPE_DEPLOY"}} -{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/19/operations/jobs.upload_failed", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/19/complete", "body": {"completion_reason": "VERSION_COMPLETE_FAILURE"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "13"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.upload_failed"}], "previous_version_id": "12", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/13/operations/jobs.upload_failed", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/13/complete", "body": {"completion_reason": "VERSION_COMPLETE_FAILURE"}} === Remote state still contains the binding whose upload failed >>> [CLI] bundle summary -o json From 8b35cb942633109951c3c8c79d2890a9d660c09d Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 9 Nov 2026 13:27:26 +0000 Subject: [PATCH 4/7] Use common feature bind expectations with DMS support --- acceptance/bundle/deployment/bind/feature/out.unbind.plain.txt | 2 -- .../bundle/deployment/bind/feature/out.unbind.recorded.txt | 3 --- acceptance/bundle/deployment/bind/feature/output.txt | 2 ++ acceptance/bundle/deployment/bind/feature/script | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) delete mode 100644 acceptance/bundle/deployment/bind/feature/out.unbind.plain.txt delete mode 100644 acceptance/bundle/deployment/bind/feature/out.unbind.recorded.txt diff --git a/acceptance/bundle/deployment/bind/feature/out.unbind.plain.txt b/acceptance/bundle/deployment/bind/feature/out.unbind.plain.txt deleted file mode 100644 index 6779f487c1f..00000000000 --- a/acceptance/bundle/deployment/bind/feature/out.unbind.plain.txt +++ /dev/null @@ -1,2 +0,0 @@ - ->>> [CLI] bundle deployment unbind feature1 diff --git a/acceptance/bundle/deployment/bind/feature/out.unbind.recorded.txt b/acceptance/bundle/deployment/bind/feature/out.unbind.recorded.txt deleted file mode 100644 index 2dace068795..00000000000 --- a/acceptance/bundle/deployment/bind/feature/out.unbind.recorded.txt +++ /dev/null @@ -1,3 +0,0 @@ - ->>> [CLI] bundle deployment unbind feature1 -Warn: Failed to record unbind history: failed to create deployment version: operations.resource_key must have a known resource type prefix (e.g. 'jobs.', 'pipelines.'): features.feature1 diff --git a/acceptance/bundle/deployment/bind/feature/output.txt b/acceptance/bundle/deployment/bind/feature/output.txt index e0bca28a059..d4d47f6a660 100644 --- a/acceptance/bundle/deployment/bind/feature/output.txt +++ b/acceptance/bundle/deployment/bind/feature/output.txt @@ -15,6 +15,8 @@ Resources: Name: main.myschema.boundfeature URL: [DATABRICKS_URL]/explore/data/features/main/myschema/boundfeature?w=[WORKSPACE_ID] +>>> [CLI] bundle deployment unbind feature1 + >>> [CLI] bundle summary Name: test-bundle Target: default diff --git a/acceptance/bundle/deployment/bind/feature/script b/acceptance/bundle/deployment/bind/feature/script index 7cb00fef219..8e27f69056c 100644 --- a/acceptance/bundle/deployment/bind/feature/script +++ b/acceptance/bundle/deployment/bind/feature/script @@ -1,5 +1,5 @@ trace $CLI bundle deployment bind feature1 main.myschema.boundfeature --auto-approve trace $CLI bundle summary -trace $CLI bundle deployment unbind feature1 2> out.unbind.$OUTPUT_MODE.txt +trace $CLI bundle deployment unbind feature1 trace $CLI bundle summary From 604170a6a777576b2ffed3060e59571c0c7a0c44 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 9 Nov 2026 17:53:10 +0000 Subject: [PATCH 5/7] Update failure expectations for direct SDK history recording --- acceptance/bundle/dms/unbind-history-failures/output.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acceptance/bundle/dms/unbind-history-failures/output.txt b/acceptance/bundle/dms/unbind-history-failures/output.txt index b7219857f28..16c1a8f26e7 100644 --- a/acceptance/bundle/dms/unbind-history-failures/output.txt +++ b/acceptance/bundle/dms/unbind-history-failures/output.txt @@ -9,7 +9,7 @@ Warn: Failed to record unbind history: failed to create deployment version: Faul >>> fault.py PATCH /api/2.0/bundle/** 400 0 1 >>> [CLI] bundle deployment unbind record_failed -Warn: Failed to record deployment history: recording operation for resources.jobs.record_failed: Fault injected by test. +Warn: Failed to record unbind history: Fault injected by test. === A version completion failure does not prevent unbinding >>> fault.py POST /api/2.0/bundle/deployments/*/versions/*/complete 400 0 1 From 2fe26a8cf02dbfa620dc2e94f2f703cafcd520fc Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Tue, 10 Nov 2026 11:10:09 +0000 Subject: [PATCH 6/7] Inline unbind history metadata selection --- bundle/direct/bind.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bundle/direct/bind.go b/bundle/direct/bind.go index 3958c5bc99a..cef864fd06a 100644 --- a/bundle/direct/bind.go +++ b/bundle/direct/bind.go @@ -278,8 +278,12 @@ func (b *DeploymentBundle) Unbind(ctx context.Context, configRoot *config.Root, return nil, err } + var configured *bool + if configRoot.Experimental != nil { + configured = configRoot.Experimental.DeploymentHistory + } var metadata *DMSUnbindMetadata - if b.dmsMetadataEnabled(ctx, configRoot) { + if b.StateDB.RecordsHistory() || bundleenv.RecordsDeploymentHistory(ctx, configured) { metadata = &DMSUnbindMetadata{Serial: b.StateDB.Data.Serial} // Finalize commits the removals below as one WAL transaction. if b.StateDB.Data.State != nil { From 3befeece33b62dde69d338105f35d731fcdc6aa4 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Fri, 13 Nov 2026 12:57:27 +0000 Subject: [PATCH 7/7] Record unbinds as updates through the shared DMS buffer --- acceptance/bundle/dms/bind-unbind/output.txt | 15 +++++++++---- .../dms/unbind-history-failures/output.txt | 16 +++++++++----- bundle/phases/bind.go | 21 +++++++++---------- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/acceptance/bundle/dms/bind-unbind/output.txt b/acceptance/bundle/dms/bind-unbind/output.txt index 84bd2d98082..22a9d7259ee 100644 --- a/acceptance/bundle/dms/bind-unbind/output.txt +++ b/acceptance/bundle/dms/bind-unbind/output.txt @@ -1,5 +1,6 @@ === Bind an existing job with deployment history enabled + >>> musterr [CLI] bundle deployment bind foo [JOB_ID] Plan: update resources.jobs.foo @@ -19,7 +20,7 @@ Successfully bound job with an id '[JOB_ID]' Run 'bundle deploy' to deploy changes to your workspace >>> print_requests.py --dms //versions --oneline -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "3"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_BIND", "resource_key": "jobs.foo"}], "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "3"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UPDATE", "resource_key": "jobs.foo"}], "version_type": "VERSION_TYPE_DEPLOY"}} {"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/3/operations/jobs.foo", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[JOB_ID]", "sequence_id": 0, "state": "{\"state\":{\"deployment\":{\"kind\":\"BUNDLE\",\"metadata_file_path\":\"/Workspace/Users/[USERNAME]/.bundle/dms-bind-unbind-[UNIQUE_NAME]/default/state/metadata.json\"},\"edit_mode\":\"UI_LOCKED\",\"format\":\"MULTI_TASK\",\"max_concurrent_runs\":1,\"name\":\"foo\",\"queue\":{\"enabled\":true}}}", "status": "OPERATION_STATUS_SUCCEEDED"}} {"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/3/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} @@ -32,13 +33,14 @@ Run 'bundle deploy' to deploy changes to your workspace >>> MSYS_NO_PATHCONV=1 [CLI] bundle-deployments list-resources deployments/[DEPLOYMENT_ID] -o json [ { - "last_action_type": "OPERATION_ACTION_TYPE_BIND", + "last_action_type": "OPERATION_ACTION_TYPE_UPDATE", "resource_id": "[JOB_ID]", "resource_key": "jobs.foo" } ] === Deploy keeps the bound job ID with recorded history + >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dms-bind-unbind-[UNIQUE_NAME]/default/files... Created jobs.foo.permissions @@ -55,10 +57,11 @@ Resources: 1 created, 1 changed, 0 deleted, 0 unchanged } === Unbind preserves the shadow history marker and leaves the job intact + >>> [CLI] bundle deployment unbind foo >>> print_requests.py --dms //versions //jobs //permissions --oneline -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "5"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.foo"}, {"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.foo.permissions"}], "previous_version_id": "4", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "5"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UPDATE", "resource_key": "jobs.foo"}, {"action_type": "OPERATION_ACTION_TYPE_UPDATE", "resource_key": "jobs.foo.permissions"}], "previous_version_id": "4", "version_type": "VERSION_TYPE_DEPLOY"}} {"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/5/operations/jobs.foo", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} {"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/5/operations/jobs.foo.permissions", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} {"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/5/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} @@ -80,11 +83,13 @@ Resources: 1 created, 1 changed, 0 deleted, 0 unchanged [] === Unbinding an already unbound resource creates no history version + >>> [CLI] bundle deployment unbind foo >>> print_requests.py --dms //versions === Rebind preserves the existing shadow history marker + >>> [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 @@ -98,11 +103,12 @@ Run 'bundle deploy' to deploy changes to your workspace } >>> print_requests.py --dms //versions --oneline -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "8"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_BIND", "resource_key": "jobs.foo"}], "previous_version_id": "5", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "8"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UPDATE", "resource_key": "jobs.foo"}], "previous_version_id": "5", "version_type": "VERSION_TYPE_DEPLOY"}} {"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/8/operations/jobs.foo", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[JOB_ID]", "sequence_id": 0, "state": "{\"state\":{\"deployment\":{\"kind\":\"BUNDLE\",\"metadata_file_path\":\"/Workspace/Users/[USERNAME]/.bundle/dms-bind-unbind-[UNIQUE_NAME]/default/state/metadata.json\"},\"edit_mode\":\"UI_LOCKED\",\"format\":\"MULTI_TASK\",\"max_concurrent_runs\":1,\"name\":\"foo\",\"queue\":{\"enabled\":true}}}", "status": "OPERATION_STATUS_SUCCEEDED"}} {"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/8/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} === A subsequent deploy continues managing the same job + >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dms-bind-unbind-[UNIQUE_NAME]/default/files... Created jobs.foo.permissions @@ -118,6 +124,7 @@ Resources: 1 created, 0 changed, 0 deleted, 1 unchanged } === A rejected duplicate bind creates no history version + >>> musterr [CLI] bundle deployment bind foo [JOB_ID] --auto-approve Error: Resource already managed diff --git a/acceptance/bundle/dms/unbind-history-failures/output.txt b/acceptance/bundle/dms/unbind-history-failures/output.txt index 16c1a8f26e7..dcfc11496db 100644 --- a/acceptance/bundle/dms/unbind-history-failures/output.txt +++ b/acceptance/bundle/dms/unbind-history-failures/output.txt @@ -1,23 +1,27 @@ === A version creation failure does not prevent unbinding + >>> fault.py POST /api/2.0/bundle/deployments/*/versions 400 0 1 >>> [CLI] bundle deployment unbind create_failed Warn: Failed to record unbind history: failed to create deployment version: Fault injected by test. === An operation recording failure does not prevent unbinding + >>> fault.py PATCH /api/2.0/bundle/** 400 0 1 >>> [CLI] bundle deployment unbind record_failed -Warn: Failed to record unbind history: Fault injected by test. +Warn: Failed to record unbind history: recording operation for resources.jobs.record_failed: Fault injected by test. === A version completion failure does not prevent unbinding + >>> fault.py POST /api/2.0/bundle/deployments/*/versions/*/complete 400 0 1 >>> [CLI] bundle deployment unbind complete_failed Warn: Failed to complete deployment history version: Fault injected by test. === Successful unbindings survived the remote state round trips and the job still exists + >>> jq .state .databricks/bundle/default/resources.json { "resources.jobs.upload_failed": { @@ -45,15 +49,16 @@ Warn: Failed to complete deployment history version: Fault injected by test. } >>> print_requests.py --dms //versions --oneline -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "10"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.create_failed"}], "previous_version_id": "9", "version_type": "VERSION_TYPE_DEPLOY"}} -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "11"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.record_failed"}], "previous_version_id": "9", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "10"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UPDATE", "resource_key": "jobs.create_failed"}], "previous_version_id": "9", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "11"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UPDATE", "resource_key": "jobs.record_failed"}], "previous_version_id": "9", "version_type": "VERSION_TYPE_DEPLOY"}} {"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/11/operations/jobs.record_failed", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} {"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/11/complete", "body": {"completion_reason": "VERSION_COMPLETE_FAILURE"}} -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "12"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.complete_failed"}], "previous_version_id": "11", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "12"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UPDATE", "resource_key": "jobs.complete_failed"}], "previous_version_id": "11", "version_type": "VERSION_TYPE_DEPLOY"}} {"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/12/operations/jobs.complete_failed", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} {"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/12/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}} === A remote state upload failure fails the unbind version + >>> fault.py POST /api/2.0/workspace-files/import-file/**resources.json 400 0 1 >>> musterr [CLI] bundle deployment unbind upload_failed @@ -66,11 +71,12 @@ API message: Fault injected by test. >>> print_requests.py --dms //versions --oneline -{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "13"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UNBIND", "resource_key": "jobs.upload_failed"}], "previous_version_id": "12", "version_type": "VERSION_TYPE_DEPLOY"}} +{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "13"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_UPDATE", "resource_key": "jobs.upload_failed"}], "previous_version_id": "12", "version_type": "VERSION_TYPE_DEPLOY"}} {"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/13/operations/jobs.upload_failed", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}} {"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/13/complete", "body": {"completion_reason": "VERSION_COMPLETE_FAILURE"}} === Remote state still contains the binding whose upload failed + >>> [CLI] bundle summary -o json { "complete_failed": null, diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index 8b6ba31b90a..cc1c08df24e 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -197,10 +197,10 @@ func Unbind(ctx context.Context, b *bundle.Bundle, group, resourceKey string, en return } - var historyVersion string + var history *bindingHistory var historyErr error defer func() { - completeBindingHistory(ctx, b, historyVersion, historyErr == nil && !logdiag.HasError(ctx)) + completeBindingHistory(ctx, b, history, historyErr == nil && !logdiag.HasError(ctx)) bundle.ApplyContext(ctx, b, lock.Release(lock.GoalUnbind)) }() @@ -219,7 +219,7 @@ func Unbind(ctx context.Context, b *bundle.Bundle, group, resourceKey string, en return } - historyVersion, historyErr = recordUnbindHistory(ctx, b, metadata) + history, historyErr = recordUnbindHistory(ctx, b, metadata) if historyErr != nil { log.Warnf(ctx, "Failed to record unbind history: %s", historyErr) } @@ -249,26 +249,25 @@ func recordBindHistory(ctx context.Context, b *bundle.Bundle, resourceKey string } // recordUnbindHistory publishes committed removals when DMS metadata was requested. -func recordUnbindHistory(ctx context.Context, b *bundle.Bundle, metadata *direct.DMSUnbindMetadata) (string, error) { +func recordUnbindHistory(ctx context.Context, b *bundle.Bundle, metadata *direct.DMSUnbindMetadata) (*bindingHistory, error) { if metadata == nil || len(metadata.ResourceKeys) == 0 { - return "", nil + return nil, nil } staged := make([]bundledeployments.StagedOperation, 0, len(metadata.ResourceKeys)) for _, key := range metadata.ResourceKeys { staged = append(staged, bundledeployments.StagedOperation{ ResourceKey: strings.TrimPrefix(key, dms.StatePrefix), - ActionType: bundledeployments.OperationActionTypeOperationActionTypeUnbind, + ActionType: bundledeployments.OperationActionTypeOperationActionTypeUpdate, }) } - versionName, err := createBindingVersion(ctx, b, metadata.Serial, staged) + history, err := createBindingVersion(ctx, b, metadata.Serial, staged) if err != nil { - return "", err + return nil, err } - var recordErr error for _, key := range metadata.ResourceKeys { - recordErr = errors.Join(recordErr, recordBindingOperation(ctx, b, versionName, key, nil)) + history.buffer.RecordOperation(ctx, key, false, "", nil) } - return versionName, recordErr + return history, history.buffer.Drain(ctx) } // createBindingVersion claims a history version for the staged binding operations.