Skip to content

Commit 482da0c

Browse files
Register existing file state for best-effort deployment history
1 parent db9b734 commit 482da0c

34 files changed

Lines changed: 1185 additions & 101 deletions

File tree

‎acceptance/bundle/dms/existing-state/output.txt‎

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,29 +25,37 @@ Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged
2525
"features": null
2626
}
2727

28-
=== Enabling recording is refused because the existing deployment is not recorded
28+
=== The environment flag enables best-effort history for the existing deployment
2929
>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true [CLI] bundle deploy
30-
Error: deployment history setting (true) does not match the existing state (false)
31-
32-
Update experimental.deployment_history to match the existing deployment, or run "databricks bundle destroy" to start over
33-
30+
Enabling deployment history...
31+
Deployment history enabled: [DATABRICKS_URL]/deployments/[DEPLOYMENT_ID]?version=2
32+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dms-existing-state-[UNIQUE_NAME]/default/files...
33+
Updated jobs.one
34+
Files: 0 uploaded, 0 deleted
35+
Resources: 0 created, 1 changed, 0 deleted, 0 unchanged
3436

3537
>>> print_requests.py --dms //api/2.0/bundle --oneline
36-
37-
=== Wiping the local cache still refuses after pulling the non-DMS state from the workspace
38+
{"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"}}}
39+
{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "2"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_INITIAL_REGISTER", "resource_key": "jobs.one"}], "version_type": "VERSION_TYPE_DEPLOY"}}
40+
{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/2/operations/jobs.one", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[ONE_ID]", "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"}}
41+
{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/2/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}}
42+
{"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.one"}], "previous_version_id": "2", "version_type": "VERSION_TYPE_DEPLOY"}}
43+
{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/3/operations/jobs.one", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[ONE_ID]", "sequence_id": 0, "state": "{\"state\":{\"deployment\":{\"deployment_id\":\"[DEPLOYMENT_ID]\",\"kind\":\"BUNDLE\",\"metadata_file_path\":\"/Workspace/Users/[USERNAME]/.bundle/dms-existing-state-[UNIQUE_NAME]/default/state/metadata.json\",\"version_id\":\"3\"},\"edit_mode\":\"UI_LOCKED\",\"format\":\"MULTI_TASK\",\"max_concurrent_runs\":1,\"name\":\"one\",\"queue\":{\"enabled\":true}}}", "status": "OPERATION_STATUS_SUCCEEDED"}}
44+
{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/3/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}}
45+
46+
=== Wiping the local cache pulls history-enabled file state without repeating registration
3847
>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true [CLI] bundle deploy
39-
Error: deployment history setting (true) does not match the existing state (false)
40-
41-
Update experimental.deployment_history to match the existing deployment, or run "databricks bundle destroy" to start over
42-
48+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dms-existing-state-[UNIQUE_NAME]/default/files...
49+
Files: 2 uploaded, 0 deleted
50+
Resources: 0 created, 0 changed, 0 deleted, 1 unchanged
4351

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

4856

4957
=== 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
50-
>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true [CLI] bundle destroy --auto-approve
58+
>>> DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=false [CLI] bundle destroy --auto-approve
5159
The following resources will be deleted:
5260
delete resources.jobs.one
5361

@@ -62,10 +70,14 @@ Files: 2 uploaded, 0 deleted
6270
Resources: 1 created, 0 changed, 0 deleted, 0 unchanged
6371

6472
>>> print_requests.py --dms //api/2.0/bundle --oneline
73+
{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "4"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_DELETE", "resource_key": "jobs.one"}], "previous_version_id": "3", "version_type": "VERSION_TYPE_DESTROY"}}
74+
{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/4/operations/jobs.one", "q": {"update_mask": "state,error_message,status"}, "body": {"error_message": "", "sequence_id": 0, "status": "OPERATION_STATUS_SUCCEEDED"}}
75+
{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/4/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}}
76+
{"method": "DELETE", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]"}
6577
{"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"}}}
66-
{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions", "q": {"version_id": "1"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_CREATE", "resource_key": "jobs.one"}], "version_type": "VERSION_TYPE_DEPLOY"}}
67-
{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/1/operations/jobs.one", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[ONE_ID]", "sequence_id": 0, "state": "{\"state\":{\"deployment\":{\"deployment_id\":\"[DEPLOYMENT_ID]\",\"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"}}
68-
{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/1/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}}
78+
{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID_2]/versions", "q": {"version_id": "1"}, "body": {"cli_version": "[CLI_VERSION]", "operations": [{"action_type": "OPERATION_ACTION_TYPE_CREATE", "resource_key": "jobs.one"}], "version_type": "VERSION_TYPE_DEPLOY"}}
79+
{"method": "PATCH", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID_2]/versions/1/operations/jobs.one", "q": {"update_mask": "state,error_message,resource_id,status"}, "body": {"error_message": "", "resource_id": "[ONE_ID_2]", "sequence_id": 0, "state": "{\"state\":{\"deployment\":{\"deployment_id\":\"[DEPLOYMENT_ID_2]\",\"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"}}
80+
{"method": "POST", "path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID_2]/versions/1/complete", "body": {"completion_reason": "VERSION_COMPLETE_SUCCESS"}}
6981

7082
>>> find.py resources[.]json[.]wal --expect 0
7183

‎acceptance/bundle/dms/existing-state/script‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,32 @@ envsubst < databricks.yml.tmpl > databricks.yml
22

33
title "Deploy without recording: ordinary direct-engine state tracking one job, unknown to DMS"
44
trace $CLI bundle deploy
5+
replace_ids.py
56
dms_plan
67
trace print_requests.py --dms //api/2.0/bundle --oneline
78
trace jq '{state_version, features}' .databricks/bundle/default/resources.json
89

9-
title "Enabling recording is refused because the existing deployment is not recorded"
10-
musterr trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deploy
10+
title "The environment flag enables best-effort history for the existing deployment"
11+
trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deploy
12+
deployment_id=$(MSYS_NO_PATHCONV=1 $CLI workspace get-status "/Workspace/Users/${CURRENT_USER_NAME}/.bundle/dms-existing-state-${UNIQUE_NAME}/default/state/resources.deployment.json" | python3 -c 'import sys, json; print(json.load(sys.stdin)["object_id"])')
13+
add_repl "$deployment_id" DEPLOYMENT_ID
1114
trace print_requests.py --dms //api/2.0/bundle --oneline
1215

13-
title "Wiping the local cache still refuses after pulling the non-DMS state from the workspace"
16+
title "Wiping the local cache pulls history-enabled file state without repeating registration"
1417
rm -rf .databricks
15-
musterr trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deploy
18+
trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deploy
1619

1720
title "A leftover local WAL with the wrong lineage is still refused"
1821
mkdir -p .databricks/bundle/default
1922
printf '{"lineage":"stale","serial":99,"state_version":2,"features":{"deployment_history_best_effort":{}}}\n' > .databricks/bundle/default/resources.json.wal
2023
musterr trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deploy
2124

2225
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"
23-
# Clear the stale WAL first: it was refused, not deleted, and destroy runs without recording, so it
26+
# Clear the stale WAL first: it was refused, not deleted, so destroy
2427
# would otherwise try to replay it and fail on the lineage mismatch.
2528
rm -f .databricks/bundle/default/resources.json.wal
2629
# Keep the mismatched setting here to verify that destroy ignores configuration and follows state.
27-
trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle destroy --auto-approve
30+
trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=false $CLI bundle destroy --auto-approve
2831
trace DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true $CLI bundle deploy
2932
trace print_requests.py --dms //api/2.0/bundle --oneline
3033
trace find.py 'resources[.]json[.]wal' --expect 0
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
bundle:
2+
name: dms-migration-before-deploy-$UNIQUE_NAME
3+
4+
experimental:
5+
deployment_history: false
6+
7+
sync:
8+
exclude:
9+
- LOG.*
10+
11+
resources:
12+
jobs:
13+
job:
14+
name: migration-before-deploy
15+
schemas:
16+
schema:
17+
name: dms_migration_before_deploy_$UNIQUE_NAME
18+
catalog_name: main
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Cloud = false
2+
EnvMatrix.DMS = ["true"]
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
2+
=== Setup (LOG.setup): create a job and schema without history
3+
Next deploy enables history and recreates the schema in another catalog, requiring approval.
4+
=== 1. Fail deployment planning
5+
Expect: migration version 2 stays complete; no deployment version is created.
6+
>>> fault.py GET /api/2.2/jobs/get* 403 0 1
7+
8+
>>> musterr [CLI] bundle deploy -qq
9+
Enabling deployment history...
10+
Deployment history enabled: [DATABRICKS_URL]/deployments/[DEPLOYMENT_ID]?version=2
11+
Error: cannot plan resources.jobs.job: reading id="[JOB_ID]": Fault injected by test. (403 INJECTED)
12+
13+
Endpoint: GET [DATABRICKS_URL]/api/2.2/jobs/get?job_id=[JOB_ID]
14+
HTTP Status: 403 Forbidden
15+
API error_code: INJECTED
16+
API message: Fault injected by test.
17+
18+
Error: planning failed
19+
20+
21+
=== Request order: complete migration -> upload full file state -> plan jobs
22+
>>> print_requests.py --dms --get //api/2.0/bundle //jobs/get //workspace/import //workspace-files/import-file --oneline
23+
{"method":"POST","path":"/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/2/complete"}
24+
{"method":"POST","path":"/Workspace/Users/[USERNAME]/.bundle/dms-migration-before-deploy-[UNIQUE_NAME]/default/state/resources.json"}
25+
{"method":"GET","path":"/api/2.2/jobs/get"}
26+
27+
=== Remote state: version 2 file state with resources; original snapshot preserved in the backup
28+
>>> MSYS_NO_PATHCONV=1 [CLI] workspace export /Workspace/Users/[USERNAME]/.bundle/dms-migration-before-deploy-[UNIQUE_NAME]/default/state/resources.json --format AUTO --file .databricks/resources.marker.json
29+
30+
>>> jq -c {serial, features, state} .databricks/resources.marker.json
31+
{"serial":2,"features":{"deployment_history_best_effort":{}},"state":{"resources.jobs.job":{"__id__":"[JOB_ID]","state":{"deployment":{"kind":"BUNDLE","metadata_file_path":"/Workspace/Users/[USERNAME]/.bundle/dms-migration-before-deploy-[UNIQUE_NAME]/default/state/metadata.json"},"edit_mode":"UI_LOCKED","format":"MULTI_TASK","max_concurrent_runs":1,"name":"migration-before-deploy","queue":{"enabled":true}}},"resources.schemas.schema":{"__id__":"[SCHEMA_ID]","state":{"catalog_name":"main","name":"dms_migration_before_deploy_[UNIQUE_NAME]"}}}}
32+
33+
>>> MSYS_NO_PATHCONV=1 [CLI] workspace export /Workspace/Users/[USERNAME]/.bundle/dms-migration-before-deploy-[UNIQUE_NAME]/default/state/resources.json.backup --format AUTO --file .databricks/resources.backup.json
34+
35+
>>> cmp .databricks/resources.before-migration.json .databricks/resources.backup.json
36+
37+
=== Migration result: only completed version 2, with INITIAL_REGISTER operations
38+
>>> MSYS_NO_PATHCONV=1 [CLI] bundle-deployments list-versions deployments/[DEPLOYMENT_ID] -o json
39+
[{"version_id":"2","status":"VERSION_STATUS_COMPLETED","completion_reason":"VERSION_COMPLETE_SUCCESS"}]
40+
41+
>>> MSYS_NO_PATHCONV=1 [CLI] bundle-deployments list-operations deployments/[DEPLOYMENT_ID]/versions/2 -o json
42+
[{"resource_key":"jobs.job","action_type":"OPERATION_ACTION_TYPE_INITIAL_REGISTER","status":"OPERATION_STATUS_SUCCEEDED"},{"resource_key":"schemas.schema","action_type":"OPERATION_ACTION_TYPE_INITIAL_REGISTER","status":"OPERATION_STATUS_SUCCEEDED"}]
43+
44+
=== 2. Retry without approval
45+
Expect: deployment declined; migration stays at version 2 and is not repeated.
46+
>>> musterr [CLI] bundle deploy -qq
47+
48+
This action will result in the deletion or recreation of the following UC schemas. Any underlying data may be lost:
49+
recreate resources.schemas.schema
50+
Error: the deployment requires destructive actions, but the current console does not support prompting.
51+
Deleting data assets such as schemas, pipelines, or volumes may cause permanent data loss and should be carefully reviewed.
52+
To proceed, use --auto-approve after reviewing the plan above.
53+
54+
55+
=== Local and remote markers must stay byte-for-byte unchanged
56+
>>> cmp .databricks/resources.marker.json .databricks/bundle/default/resources.json
57+
58+
>>> MSYS_NO_PATHCONV=1 [CLI] workspace export /Workspace/Users/[USERNAME]/.bundle/dms-migration-before-deploy-[UNIQUE_NAME]/default/state/resources.json --format AUTO --file .databricks/resources.after-decline.json
59+
60+
>>> cmp .databricks/resources.marker.json .databricks/resources.after-decline.json
61+
62+
=== No DMS writes and no extra version on the declined retry
63+
>>> print_requests.py --dms //api/2.0/bundle
64+
65+
>>> MSYS_NO_PATHCONV=1 [CLI] bundle-deployments list-versions deployments/[DEPLOYMENT_ID] -o json
66+
[{"version_id":"2","status":"VERSION_STATUS_COMPLETED","completion_reason":"VERSION_COMPLETE_SUCCESS"}]
67+
68+
=== 3. Approve deployment
69+
Expect: ordinary deployment version 3; version 2 remains the only migration.
70+
>>> [CLI] bundle deploy --auto-approve -qq
71+
72+
This action will result in the deletion or recreation of the following UC schemas. Any underlying data may be lost:
73+
recreate resources.schemas.schema
74+
75+
>>> [CLI] bundle summary -o json
76+
{"latest_version_id":3}
77+
78+
>>> MSYS_NO_PATHCONV=1 [CLI] bundle-deployments list-versions deployments/[DEPLOYMENT_ID] -o json
79+
[{"version_id":"2","status":"VERSION_STATUS_COMPLETED","completion_reason":"VERSION_COMPLETE_SUCCESS"},{"version_id":"3","status":"VERSION_STATUS_COMPLETED","completion_reason":"VERSION_COMPLETE_SUCCESS"}]

0 commit comments

Comments
 (0)