bundle: Clear local state on destroy to prevent lineage mismatch - #6210
Conversation
Integration test reportCommit: 5d6ee06
Top 8 slowest tests (at least 2 minutes):
|
Destroy only deleted the remote state, leaving the local state file (and its lineage) behind. When the same bundle was later redeployed from a different machine that had no local state, the fresh deploy minted a new lineage that no longer matched the lingering local one, so every subsequent command failed with "lineage mismatch in state files". Remove the local state files on a successful destroy so no stale lineage is left behind; the next command then adopts the remote state cleanly. Both engines keep a lineage-bearing state file (direct: resources.json, terraform: terraform.tfstate) and both are read by validateStates, so remove both regardless of which engine ran the destroy. Reworks bundle/state/state_present, which relied on the local direct state surviving a destroy to assert serial continuity. Its engine- selection intent is unchanged; the mid-test destroy is dropped. The reproducing test now runs on both engines. Co-authored-by: Isaac
f28302d to
5445a04
Compare
…ineage-on-destroy # Conflicts: # acceptance/bundle/destroy/lineage-mismatch-after-redeploy/out.test.toml # acceptance/bundle/destroy/lineage-mismatch-after-redeploy/output.txt # acceptance/bundle/destroy/lineage-mismatch-after-redeploy/script # acceptance/bundle/destroy/lineage-mismatch-after-redeploy/test.toml
Now that destroy removes the local state file, read_state.py called after a destroy (e.g. bundle/resources/pipelines/update) hit FileNotFoundError on its unconditional open(). Treat a missing state file the same as a missing entry within the file and print "State not found", matching print_state.py. Co-authored-by: Isaac
| // Remove the local state files now that the deployment is gone. Destroy only | ||
| // deletes the remote state; leaving a local state file behind keeps its | ||
| // lineage around, so a later fresh deploy of the same bundle (e.g. from | ||
| // another machine that has no local state) mints a new lineage that no longer | ||
| // matches this lingering one, and every subsequent command fails with a | ||
| // lineage mismatch. Both engines keep a lineage-bearing state file, so remove | ||
| // both regardless of which engine ran the destroy. | ||
| _, localDirectPath := b.StateFilenameDirect(ctx) | ||
| _, localTerraformPath := b.StateFilenameTerraform(ctx) |
There was a problem hiding this comment.
destroy can only run on one engine at a time. we know the engine, so we should only delete the appropriate one here and not touch other files
There was a problem hiding this comment.
Done in 6e9baa7 — now selects the running engine's state file (direct: resources.json, terraform: terraform.tfstate) and removes only that one.
Destroy runs on a single engine, which we already know here, so remove only that engine's local state file instead of both. Addresses review feedback. Co-authored-by: Isaac
…ineage-on-destroy # Conflicts: # acceptance/bundle/destroy/lineage-mismatch-after-redeploy/script # acceptance/bundle/state/state_present/output.txt # bundle/phases/destroy.go
…ineage-on-destroy # Conflicts: # acceptance/bundle/destroy/lineage-mismatch-after-redeploy/out.test.toml
read_state.py now names the state file it consulted in the "State not found" message; normalize that engine-specific path in pipelines/update so the terraform and direct matrix variants stay identical. Rework the lineage-mismatch test to keep the reproduction (a lingering local state still mismatches after an out-of-band redeploy) alongside a new assertion that a clean destroy removes the local state file. Run it on both engines, recording the engine-specific mismatch error in out.mismatch.<engine>.txt. Co-authored-by: Isaac <no-reply@databricks.com>
…ineage-on-destroy Co-authored-by: Isaac <no-reply@databricks.com>
Keeping the terraform deploy/destroy pair, the destroy now runs on the higher-serial direct state and removes it (the local-state cleanup this PR adds). The following direct deploy then migrates from the leftover terraform state instead of staying on direct, so assert that behavior and ignore the terraform provider artifacts the deploy leaves behind. Co-authored-by: Isaac <no-reply@databricks.com>
…ineage-on-destroy
… files The mismatch error names the engine-specific state file (resources.json vs terraform.tfstate). Normalize it to [STATE_FILE]/[ENGINE] via a scoped Repl so both matrix variants share one output.txt, dropping the out.mismatch.<engine>.txt split. Co-authored-by: Isaac <no-reply@databricks.com>
A fresh-create plan no longer carries a lineage (it is minted at deploy, not plan), matching main. Regenerate the stale golden. Co-authored-by: Isaac <no-reply@databricks.com>
| { | ||
| "plan_version": [PLAN_VERSION], | ||
| "cli_version": "[CLI_VERSION]", | ||
| "lineage": "[UUID]", |
There was a problem hiding this comment.
this got removed because the plan is captured after the destroy
shreyas-goenka
left a comment
There was a problem hiding this comment.
minor comments, thanks!
| Destroy: 1 deleted | ||
|
|
||
| >>> DATABRICKS_BUNDLE_ENGINE=direct [CLI] bundle deploy | ||
| Warn: Direct engine selected via DATABRICKS_BUNDLE_ENGINE environment variable but the existing state uses "terraform". Deploying on "terraform"; will attempt to migrate the state to the direct engine after this deploy. |
There was a problem hiding this comment.
just curious, why is this warning showing up now but was not present before? Unexpected diff?
There was a problem hiding this comment.
There should not be a TF state at this point?
There was a problem hiding this comment.
before we cleaned up local state on destroy:
remote direct state gone, local direct state remained, DATABRICKS_BUNDLE_ENGINE=direct deploy is deployed as direct because the higher .serial wins.
after removing local state:
destroy runs on direct (higher serial) and deletes remote and local direct states, so only terraform/terraform.tfstate remains. per existing logic, DATABRICKS_BUNDLE_ENGINE=direct deploy deploys as TF.
There was a problem hiding this comment.
We just ran bundle destroy before running deploy. That should have removed terraform/terraform.tfstate right (given we are removing local state).
The recorded (DMS) path reads deployment history, not a file, so it cannot print a filename; keeping it on the file-based paths made the pipelines/update DMS matrix variants diverge. Drop the filename for now (revisit denik's suggestion later) and remove the now-dead [STATE_FILE] Repl. Co-authored-by: Isaac <no-reply@databricks.com>
directories around it (.internal/, sync-snapshots/) are left behind empty. Prune every directory that ends up empty under the bundle+target local state directory. Symlinks count as content and are not traversed, so terraform's provider mirror survives, and the walk stays within the target's state directory so a sibling engine's state is untouched. Follow-up to #6210 (comment) Co-authored-by: Isaac <no-reply@databricks.com>
- Replace the `find -type d -empty` / hand-rolled snapshot checks with a full `find` of the state tree in both destroy acceptance tests, so a stray file and an empty scaffolding dir both show up. Prune the terraform engine dir: its contents differ by engine and embed the provider version and OS/arch, which would make the golden fragile; the bundle scaffolding under test lives directly under the target dir. - Fold the empty-dir cleanup into the clear-lineage-on-destroy changelog fragment (spanning #6210 and #6685) and drop the separate fragment. Co-authored-by: Isaac <no-reply@databricks.com>
databricks#6210 makes `bundle destroy` remove the local state file, but the directories around it (`.internal/`, `sync-snapshots/`) are left behind empty. This PR has `destroy` prune every directory that ends up empty under the bundle+target local state directory. Symlinks count as content and are not traversed, so terraform's provider mirror survives, and the walk stays within the target's state directory so a sibling engine's state is untouched. Follow-up to databricks#6210 (comment) --------- Co-authored-by: Isaac <no-reply@databricks.com>
## Release v1.17.0 ### Notable Changes * Bump the direct deployment state version to 3. Clients older than v1.8.0 will reject bundles deployed with this release. ([#6713](#6713)) ### CLI * Add an `INVALID_REFRESH_TOKEN` error code to `databricks auth token --output json` failures. ([#6684](#6684)) * Add experimental `databricks auth docker configure` to configure Docker credential helper access for Databricks Artifact Registry. ([#6700](#6700)) * Add experimental `databricks auth docker token` to generate Docker credentials for Databricks Artifact Registry. ([#6699](#6699)) * `databricks environments setup-local` now reports the `E_PROVISION_CONFLICT` error code instead of the generic `E_PROVISION` when `uv sync` fails to resolve a dependency conflict. ([#6666](#6666)) * Preserve SSH sessions across temporary tunnel disconnects, with bounded replay and backpressure for large transfers. ([#6650](#6650)) * Allow OAuth U2M logins to override the CLI client ID with `--client-id`, profile `client_id`, or `DATABRICKS_CLIENT_ID`. ([#6594](#6594)) ### Bundles * direct: Store a dashboard's `serialized_dashboard` in state as a content hash instead of its full contents. ([#6105](#6105)) * direct: Fix pipelines recreation when the whole `ingestion_definition` block is added or removed. ([#6589](#6589)) * `bundle plan`, `deploy`, and `destroy` no longer report removing `permissions`, `grants`, or secret scope ACLs from a bundle as a deletion, since it leaves the resource untouched. ([#6647](#6647)) * `bundle plan` and `deploy` no longer list or count a resource that was already deleted remotely as a deletion, matching `bundle destroy`; applying still cleans up its stale state entry. ([#6675](#6675)) * Fix `bundle run` failing with `expected an int, found a string` when an unrelated resource references another resource that is not deployed. `bundle run` now resolves `${resources.*}` references only within the resource being run. ([#6690](#6690)) * Add grants support for the AI Gateway `model_service`, `mcp_service`, and `model_provider_service` resources (direct engine). ([#6635](#6635)) * Add bundle support for the AI Gateway `mcp_service` resource (direct engine). ([#6633](#6633)) * Add bundle support for the AI Gateway `model_provider_service` resource (direct engine). ([#6634](#6634)) * Add bundle support for the AI Gateway `model_service` resource (direct engine). ([#6525](#6525)) * Prevent resource drift on catalogs if `storage_root` contained a trailing slash in the URL. ([#6622](#6622)) * Fixed a "lineage mismatch in state files" error that could occur after destroying a bundle and redeploying it from another machine. `bundle destroy` now removes the local state file so no stale lineage is left behind, and prunes the state directories it leaves empty (such as `.internal/` and `sync-snapshots/`). ([#6210](#6210), [#6685](#6685)) * direct: `bundle plan` no longer reports a permanent update on a cluster that uses a cluster policy: when the cluster spec sets `policy_id`, a field present in the remote but absent from the bundle config is not treated as drift. ([#6531](#6531)) * `bundle deploy` on the direct engine now reports each resource as soon as it is deployed, instead of listing them all after the deployment finishes. A deploy that fails part way through now reports the resources it did apply. ([#6361](#6361)) * Direct-engine bundles no longer flag phantom drift on server-populated nested fields under reused config types (e.g. `external_locations` file-event-queue resource IDs, `database_instances` parent-instance refs, `apps` git credential ID). ([#6618](#6618)) * `databricks bundle generate app` now reproduces a git-backed app's `git_repository` and `git_source` configuration instead of emitting a workspace `source_code_path`, so generating from a Git-deployed app no longer silently converts it to workspace source. ([#6656](#6656)) * Improved configuration load time for bundles with many included files. ([#6195](#6195)) * `bundle destroy` no longer deletes triggered job runs, leaving them untouched on the backend. ([#6672](#6672)) * direct: resources.job\_runs: new lifecycle.triggers.on\_file\_change setting to restart the run when monitored files change. Can be set to a series of paths or globs. ([#6309](#6309)) * Bundle summary now shows a name for Postgres branches, endpoints, databases, and roles instead of a blank Name field. ([#6663](#6663)) * Added PyDABs (Python) support for cluster policies, dashboards, and Genie spaces. ([#6585](#6585)) * CLI commands no longer imply that a resource whose type has no workspace URL is merely not deployed yet. ([#6583](#6583)) * Capture the implicit dependency a vector search index has on a catalog or schema defined in the same bundle, so the catalog and schema are deployed first. ([#6655](#6655)) ### Dependency Updates * Bump dependencies with known vulnerabilities. ([#6695](#6695)) * Bump `github.com/databricks/databricks-sdk-go` from v0.177.0 to v0.178.0. ([#6673](#6673)) * Bump Terraform provider from v1.131.0 to v1.132.0. ([#6671](#6671))
Make the destroy summary line ("Destroy: N deleted") print even when the
destroy encounters errors partway through. This is a follow-up to PR #6210.
Before this change, early returns on errors (during the resource Apply, DMS
finalization, or file deletion) skipped the summary entirely. Extract the
summary into logDestroySummary and invoke it via defer in destroyCore so it
prints on every return path; partial deletions may have already succeeded.
The count is the planned deletions, not the actually-succeeded ones. Making
the count reflect only successful deletions is a separate follow-up.
Add an acceptance test that injects a failed resource delete and asserts the
summary still prints after the destroy error. The dms/failed-delete golden
gains the same summary line for the same reason.
Co-authored-by: Isaac <no-reply@databricks.com>
Follow-up to #6207 (which adds the reproducing acceptance test).
bundle destroynow deletes local state (resources.json in direct, terraform.tfstate in TF).