Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nextchanges/bundles/direct-dms-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Add `bundle deployment migrate --enable-history` to migrate existing Direct deployments to deployment history. ([#6950](https://github.com/databricks/cli/pull/6950))
59 changes: 19 additions & 40 deletions acceptance/bundle/dms/existing-state/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,49 +25,18 @@ Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged
"features": null
}

=== Enabling recording is refused because the existing deployment is not recorded
>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true [CLI] bundle deploy
Error: deployment history setting (true) does not match the existing state (false)
=== Migrate the existing deployment to DMS without applying resource changes
>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true [CLI] bundle deployment migrate --enable-history
Migrate: 1 resources moved to deployment history

Update experimental.deployment_history to match the existing deployment, or run "databricks bundle destroy" to start over


>>> print_requests.py --dms //api/2.0/bundle --oneline

=== Wiping the local cache still refuses after pulling the non-DMS state from the workspace
>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true [CLI] bundle deploy
Error: deployment history setting (true) does not match the existing state (false)

Update experimental.deployment_history to match the existing deployment, or run "databricks bundle destroy" to start over


=== A leftover local WAL with the wrong lineage is still refused
>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true [CLI] bundle deploy
Error: reading state from [TEST_TMP_DIR]/.databricks/bundle/default/resources.json: WAL recovery failed: WAL lineage ("stale") does not match state lineage ("[UUID]")


=== Destroy clears the deployment. With nothing recorded remotely the bundle is treated as new, so recording can be enabled once the stale WAL is gone
>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true [CLI] bundle destroy --auto-approve
The following resources will be deleted:
delete resources.jobs.one

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

Destroy: 1 deleted

>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dms-existing-state-[UNIQUE_NAME]/default/files...
Created jobs.one
Files: 2 uploaded, 0 deleted
Resources: 1 created, 0 changed, 0 deleted, 0 unchanged

>>> print_requests.py --dms //api/2.0/bundle --oneline
>>> print_requests.py --dms --keep //api/2.0/bundle --oneline
{"method": "POST", "path": "/api/2.0/bundle/deployments", "body": {"display_name": "dms-existing-state-[UNIQUE_NAME]", "initial_parent_path": "/Workspace/Users/[USERNAME]/.bundle/dms-existing-state-[UNIQUE_NAME]/default/state", "target_name": "default", "workspace_info": {"file_path": "/Workspace/Users/[USERNAME]/.bundle/dms-existing-state-[UNIQUE_NAME]/default/files", "root_path": "/Workspace/Users/[USERNAME]/.bundle/dms-existing-state-[UNIQUE_NAME]/default"}}}
{"method": "POST", "path": "/api/2.0/bundle/deployments/[NUMID]/versions", "q": {"version_id": "1"}, "body": {"cli_version": "[CLI_VERSION]", "version_type": "VERSION_TYPE_DEPLOY", "operations": [{"resource_key": "jobs.one", "action_type": "OPERATION_ACTION_TYPE_CREATE"}]}}
{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[NUMID]/versions/1/operations/jobs.one", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[NUMID]", "sequence_id": "0", "state": "{\"state\":{\"deployment\":{\"deployment_id\":\"[NUMID]\",\"kind\":\"BUNDLE\",\"metadata_file_path\":\"/Workspace/Users/[USERNAME]/.bundle/dms-existing-state-[UNIQUE_NAME]/default/state/metadata.json\",\"version_id\":\"1\"},\"edit_mode\":\"UI_LOCKED\",\"format\":\"MULTI_TASK\",\"max_concurrent_runs\":1,\"name\":\"one\",\"queue\":{\"enabled\":true}}}", "status": "OPERATION_STATUS_SUCCEEDED"}}
{"method": "POST", "path": "/api/2.0/bundle/deployments/[NUMID]/versions/1/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}}
{"method": "POST", "path": "/api/2.0/bundle/deployments/[NUMID]/versions", "q": {"version_id": "2"}, "body": {"cli_version": "[CLI_VERSION]", "version_type": "VERSION_TYPE_DEPLOY", "operations": [{"resource_key": "jobs.one", "action_type": "OPERATION_ACTION_TYPE_MIGRATE"}]}}
{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[NUMID]/versions/2/operations/jobs.one", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[NUMID]", "sequence_id": "0", "state": "{\"state\":{\"deployment\":{\"kind\":\"BUNDLE\",\"metadata_file_path\":\"/Workspace/Users/[USERNAME]/.bundle/dms-existing-state-[UNIQUE_NAME]/default/state/metadata.json\"},\"edit_mode\":\"UI_LOCKED\",\"format\":\"MULTI_TASK\",\"max_concurrent_runs\":1,\"name\":\"one\",\"queue\":{\"enabled\":true}}}", "status": "OPERATION_STATUS_SUCCEEDED"}}
{"method": "POST", "path": "/api/2.0/bundle/deployments/[NUMID]/versions/2/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}}

>>> find.py resources[.]json[.]wal --expect 0
>>> print_requests.py --keep --del-field raw_body //resources.json.backup --oneline
{"method": "POST", "path": "/api/2.0/workspace-files/import-file/Workspace/Users/[USERNAME]/.bundle/dms-existing-state-[UNIQUE_NAME]/default/state/resources.json.backup", "q": {"overwrite": "true"}, "body": {"state_version": 3, "cli_version": "[CLI_VERSION]", "lineage": "[UUID]", "serial": 1, "state": {"resources.jobs.one": {"__id__": "[NUMID]", "state": {"deployment": {"kind": "BUNDLE", "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/dms-existing-state-[UNIQUE_NAME]/default/state/metadata.json"}, "edit_mode": "UI_LOCKED", "format": "MULTI_TASK", "max_concurrent_runs": 1, "name": "one", "queue": {"enabled": true}}}}}}

>>> jq {state_version, features} .databricks/bundle/default/resources.json
{
Expand All @@ -76,3 +45,13 @@ Resources: 1 created, 0 changed, 0 deleted, 0 unchanged
"deployment_history": {}
}
}

=== The next plan adds deployment metadata without recreating resources
>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true [CLI] bundle plan
update jobs.one

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

=== Migration is idempotent after DMS becomes authoritative
>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true [CLI] bundle deployment migrate --enable-history
Deployment already records deployment history.
32 changes: 10 additions & 22 deletions acceptance/bundle/dms/existing-state/script
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,18 @@ dms_plan
trace print_requests.py --dms //api/2.0/bundle --oneline
trace jq '{state_version, features}' .databricks/bundle/default/resources.json

title "Enabling recording is refused because the existing deployment is not recorded"
musterr trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deploy
trace print_requests.py --dms //api/2.0/bundle --oneline

title "Wiping the local cache still refuses after pulling the non-DMS state from the workspace"
rm -rf .databricks
musterr trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deploy
title "Migrate the existing deployment to DMS without applying resource changes"
trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deployment migrate --enable-history
trace print_requests.py --dms --keep //api/2.0/bundle --oneline
trace print_requests.py --keep --del-field raw_body //resources.json.backup --oneline
trace jq '{state_version, features}' .databricks/bundle/default/resources.json

title "A leftover local WAL with the wrong lineage is still refused"
mkdir -p .databricks/bundle/default
printf '{"lineage":"stale","serial":99,"state_version":2,"features":{"deployment_history":{}}}\n' > .databricks/bundle/default/resources.json.wal
musterr trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deploy
title "The next plan adds deployment metadata without recreating resources"
trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle plan

title "Destroy clears the deployment. With nothing recorded remotely the bundle is treated as new, so recording can be enabled once the stale WAL is gone"
# Clear the stale WAL first: it was refused, not deleted, and destroy runs without recording, so it
# would otherwise try to replay it and fail on the lineage mismatch.
rm -f .databricks/bundle/default/resources.json.wal
# Keep the mismatched setting here to verify that destroy ignores configuration and follows state.
trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle destroy --auto-approve
trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deploy
trace print_requests.py --dms //api/2.0/bundle --oneline
trace find.py 'resources[.]json[.]wal' --expect 0
trace jq '{state_version, features}' .databricks/bundle/default/resources.json
title "Migration is idempotent after DMS becomes authoritative"
trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deployment migrate --enable-history

# The recorded requests are asserted above via print_requests.py; drop whatever the run added after
# that (telemetry, and the state probes of the non-recording variant) so nothing is left to compare.
# that (telemetry and read-only probes) so nothing is left to compare.
rm -f "$OUT_REQUESTS"
9 changes: 9 additions & 0 deletions acceptance/bundle/dms/migration-resume/databricks.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
bundle:
name: dms-migration-resume-$UNIQUE_NAME

resources:
jobs:
one:
name: one
two:
name: two
2 changes: 2 additions & 0 deletions acceptance/bundle/dms/migration-resume/out.test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Cloud = true
EnvMatrix.DMS = [""]
56 changes: 56 additions & 0 deletions acceptance/bundle/dms/migration-resume/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@

=== Create a direct deployment with two resources
>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dms-migration-resume-[UNIQUE_NAME]/default/files...
Created jobs.one
Created jobs.two
Files: 2 uploaded, 0 deleted
Resources: 2 created, 0 changed, 0 deleted, 0 unchanged

=== A failed operation leaves resources.json authoritative
>>> fault.py PATCH /api/2.0/bundle/* 500 0 1

>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true [CLI] bundle deployment migrate --enable-history
Error: recording migration resources: recording operation for resources.jobs.one: Fault injected by test. (500 INJECTED)

Endpoint: PATCH [DATABRICKS_URL]/api/2.0/bundle/deployments/[NUMID]/versions/2/operations/jobs.one?update_mask=state%2Cerror_message%2Cresource_id%2Cstatus
HTTP Status: 500 Internal Server Error
API error_code: INJECTED
API message: Fault injected by test.


>>> jq {serial, features, resources: (.state | keys)} .databricks/bundle/default/resources.json
{
"serial": 1,
"features": null,
"resources": [
"resources.jobs.one",
"resources.jobs.two"
]
}

=== Retry migrates only the resource missing from DMS
>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true [CLI] bundle deployment migrate --enable-history
Migrate: 2 resources moved to deployment history

>>> print_requests.py --dms --keep //api/2.0/bundle --oneline
{"method": "POST", "path": "/api/2.0/bundle/deployments", "body": {"display_name": "dms-migration-resume-[UNIQUE_NAME]", "initial_parent_path": "/Workspace/Users/[USERNAME]/.bundle/dms-migration-resume-[UNIQUE_NAME]/default/state", "target_name": "default", "workspace_info": {"file_path": "/Workspace/Users/[USERNAME]/.bundle/dms-migration-resume-[UNIQUE_NAME]/default/files", "root_path": "/Workspace/Users/[USERNAME]/.bundle/dms-migration-resume-[UNIQUE_NAME]/default"}}}
{"method": "POST", "path": "/api/2.0/bundle/deployments/[NUMID]/versions", "q": {"version_id": "2"}, "body": {"cli_version": "[CLI_VERSION]", "version_type": "VERSION_TYPE_DEPLOY", "operations": [{"resource_key": "jobs.one", "action_type": "OPERATION_ACTION_TYPE_MIGRATE"}, {"resource_key": "jobs.two", "action_type": "OPERATION_ACTION_TYPE_MIGRATE"}]}}
{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[NUMID]/versions/2/operations/jobs.one", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[NUMID]", "sequence_id": "0", "state": "{\"state\":{\"deployment\":{\"kind\":\"BUNDLE\",\"metadata_file_path\":\"/Workspace/Users/[USERNAME]/.bundle/dms-migration-resume-[UNIQUE_NAME]/default/state/metadata.json\"},\"edit_mode\":\"UI_LOCKED\",\"format\":\"MULTI_TASK\",\"max_concurrent_runs\":1,\"name\":\"one\",\"queue\":{\"enabled\":true}}}", "status": "OPERATION_STATUS_SUCCEEDED"}}
{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[NUMID]/versions/2/operations/jobs.two", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[NUMID]", "sequence_id": "0", "state": "{\"state\":{\"deployment\":{\"kind\":\"BUNDLE\",\"metadata_file_path\":\"/Workspace/Users/[USERNAME]/.bundle/dms-migration-resume-[UNIQUE_NAME]/default/state/metadata.json\"},\"edit_mode\":\"UI_LOCKED\",\"format\":\"MULTI_TASK\",\"max_concurrent_runs\":1,\"name\":\"two\",\"queue\":{\"enabled\":true}}}", "status": "OPERATION_STATUS_SUCCEEDED"}}
{"method": "POST", "path": "/api/2.0/bundle/deployments/[NUMID]/versions/2/complete", "body": {"completion_reason": "VERSION_COMPLETE_FAILURE"}}
{"method": "POST", "path": "/api/2.0/bundle/deployments/[NUMID]/versions", "q": {"version_id": "3"}, "body": {"cli_version": "[CLI_VERSION]", "version_type": "VERSION_TYPE_DEPLOY", "previous_version_id": "2", "operations": [{"resource_key": "jobs.one", "action_type": "OPERATION_ACTION_TYPE_MIGRATE"}]}}
{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[NUMID]/versions/3/operations/jobs.one", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[NUMID]", "sequence_id": "0", "state": "{\"state\":{\"deployment\":{\"kind\":\"BUNDLE\",\"metadata_file_path\":\"/Workspace/Users/[USERNAME]/.bundle/dms-migration-resume-[UNIQUE_NAME]/default/state/metadata.json\"},\"edit_mode\":\"UI_LOCKED\",\"format\":\"MULTI_TASK\",\"max_concurrent_runs\":1,\"name\":\"one\",\"queue\":{\"enabled\":true}}}", "status": "OPERATION_STATUS_SUCCEEDED"}}
{"method": "POST", "path": "/api/2.0/bundle/deployments/[NUMID]/versions/3/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}}

>>> print_requests.py --keep --del-field raw_body //resources.json.backup --oneline
{"method": "POST", "path": "/api/2.0/workspace-files/import-file/Workspace/Users/[USERNAME]/.bundle/dms-migration-resume-[UNIQUE_NAME]/default/state/resources.json.backup", "q": {"overwrite": "true"}, "body": {"state_version": 3, "cli_version": "[CLI_VERSION]", "lineage": "[UUID]", "serial": 1, "state": {"resources.jobs.one": {"__id__": "[NUMID]", "state": {"deployment": {"kind": "BUNDLE", "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/dms-migration-resume-[UNIQUE_NAME]/default/state/metadata.json"}, "edit_mode": "UI_LOCKED", "format": "MULTI_TASK", "max_concurrent_runs": 1, "name": "one", "queue": {"enabled": true}}}, "resources.jobs.two": {"__id__": "[NUMID]", "state": {"deployment": {"kind": "BUNDLE", "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/dms-migration-resume-[UNIQUE_NAME]/default/state/metadata.json"}, "edit_mode": "UI_LOCKED", "format": "MULTI_TASK", "max_concurrent_runs": 1, "name": "two", "queue": {"enabled": true}}}}}}

>>> jq {serial, features, resources: (.state | keys)} .databricks/bundle/default/resources.json
{
"serial": null,
"features": {
"deployment_history": {}
},
"resources": []
}
18 changes: 18 additions & 0 deletions acceptance/bundle/dms/migration-resume/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
envsubst < databricks.yml.tmpl > databricks.yml

title "Create a direct deployment with two resources"
trace $CLI bundle deploy
rm -f "$OUT_REQUESTS"

title "A failed operation leaves resources.json authoritative"
trace fault.py "PATCH /api/2.0/bundle/*" 500 0 1
musterr trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deployment migrate --enable-history
trace jq '{serial, features, resources: (.state | keys)}' .databricks/bundle/default/resources.json

title "Retry migrates only the resource missing from DMS"
trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deployment migrate --enable-history
trace print_requests.py --dms --keep //api/2.0/bundle --oneline
trace print_requests.py --keep --del-field raw_body //resources.json.backup --oneline
trace jq '{serial, features, resources: (.state | keys)}' .databricks/bundle/default/resources.json

rm -f "$OUT_REQUESTS"
2 changes: 2 additions & 0 deletions acceptance/bundle/dms/migration-resume/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This test creates a direct state first and enables DMS only for the migration command.
EnvMatrix.DMS = [""]
9 changes: 7 additions & 2 deletions acceptance/bundle/help/bundle-deployment-migrate/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ lineage and incremented serial number.
Note, the migration is performed locally only. To finalize it, run 'bundle deploy'. This will synchronize the state file
to the workspace so that subsequent deploys of this bundle use direct deployment engine as well.

With --enable-history, this command instead migrates an existing Direct deployment
to the deployment metadata service. The migration completes before any later deploy,
and resources.json is backed up before DMS becomes authoritative.

Usage:
databricks bundle deployment migrate [flags]

Flags:
-h, --help help for migrate
--noplancheck No-op (kept for compatibility).
--enable-history Migrate an existing Direct deployment to deployment history.
-h, --help help for migrate
--noplancheck No-op (kept for compatibility).

Global Flags:
--debug enable debug logging
Expand Down
5 changes: 2 additions & 3 deletions acceptance/bundle/migrate/auto/dms-migration/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

=== Direct + deployment-history requested against a terraform state is rejected before commit
>>> DATABRICKS_BUNDLE_ENGINE=direct DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true errcode [CLI] bundle deploy
Notice: migrating your bundle to direct deployment engine (https://github.com/databricks/cli/issues/6765).
Error: deployment history setting (true) does not match the existing state (false)
Error: cannot migrate Terraform state directly to deployment history

Update experimental.deployment_history to match the existing deployment, or run "databricks bundle destroy" to start over
First disable experimental.deployment_history and run "databricks bundle deploy" to migrate to Direct state. Then re-enable deployment history and run "databricks bundle deployment migrate --enable-history"


Exit code: 1
Expand Down
3 changes: 2 additions & 1 deletion bundle/deploy/lock/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Goal string
const (
GoalBind = Goal("bind")
GoalUnbind = Goal("unbind")
GoalMigrate = Goal("migrate")
GoalDeploy = Goal("deploy")
GoalDestroy = Goal("destroy")
)
Expand Down Expand Up @@ -48,7 +49,7 @@ func (m *release) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics
switch m.goal {
case GoalDeploy:
return diag.FromErr(b.Locker.Unlock(ctx))
case GoalBind, GoalUnbind:
case GoalBind, GoalUnbind, GoalMigrate:
return diag.FromErr(b.Locker.Unlock(ctx))
case GoalDestroy:
// Destroy may have proceeded without acquiring a lock (see lock.Acquire:
Expand Down
Loading
Loading