Skip to content

direct: new 'migrate' impl that looks up values from tfstate without reading resources - #5399

Merged
denik merged 38 commits into
mainfrom
denik/new-migration
Jun 24, 2026
Merged

direct: new 'migrate' impl that looks up values from tfstate without reading resources#5399
denik merged 38 commits into
mainfrom
denik/new-migration

Conversation

@denik

@denikdenik commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Changes

  • Change "deployment migrate" command to read tfstate for cases when value is not available in config ($resources references).
  • Remove plan check and recommendation to do plan before deploy (--noplancheck becomes no-op for compatibility).

Why

Not doing network access means it's faster and less likely to fail, which is something we need for auto-migration.

Tests

Existing tests.

@denik
deniktemporarily deployed to test-trigger-is June 1, 2026 15:27 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 1, 2026 15:27 — with GitHub Actions Inactive
@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: 730b256

Run: 28095821493

Env🟨​KNOWN🔄​flaky💚​RECOVERED🙈​SKIP✅​pass🙈​skipTime
🟨​aws linux71324410245:24
🟨​aws windows71324610227:59
💚​aws-ucws linux7133349404:53
💚​aws-ucws windows7133369386:27
💚​azure linux11524710224:54
💚​azure windows11524910205:55
💚​azure-ucws linux1153399365:47
🔄​azure-ucws windows21153399346:27
💚​gcp linux11524610244:15
💚​gcp windows11524810225:50
22 interesting tests: 13 SKIP, 7 KNOWN, 2 flaky
Test Nameaws linuxaws windowsaws-ucws linuxaws-ucws windowsazure linuxazure windowsazure-ucws linuxazure-ucws windowsgcp linuxgcp windows
🟨​TestAccept🟨​K🟨​K💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R
🙈​TestAccept/bundle/invariant/no_drift🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/permissions🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🟨​TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions🟨​K🟨​K💚​R💚​R🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🟨​TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=direct🟨​K🟨​K💚​R💚​R
🟨​TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=terraform🟨​K🟨​K💚​R💚​R
🟨​TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions🟨​K🟨​K💚​R💚​R🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🟨​TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=direct🟨​K🟨​K💚​R💚​R
🟨​TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=terraform🟨​K🟨​K💚​R💚​R
🙈​TestAccept/bundle/resources/postgres_branches/basic🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/postgres_branches/recreate🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/postgres_branches/replace_existing🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/postgres_branches/update_protected🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/postgres_branches/without_branch_id🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/postgres_endpoints/basic🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/postgres_projects/update_display_name🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/synced_database_tables/basic🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/ssh/connection🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🔄​TestFsMkdir✅​p✅​p✅​p✅​p✅​p✅​p✅​p🔄​f✅​p✅​p
🔄​TestFsMkdir/dbfs✅​p✅​p✅​p✅​p✅​p✅​p✅​p🔄​f✅​p✅​p
Top 4 slowest tests (at least 2 minutes):
durationenvtestname
3:46gcp windowsTestAccept
3:33aws-ucws windowsTestAccept
3:29azure windowsTestAccept
3:16azure-ucws windowsTestAccept

@denik
deniktemporarily deployed to test-trigger-is June 2, 2026 12:12 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 2, 2026 12:12 — with GitHub Actions Inactive
@denik
denikforce-pushed the denik/new-migration branch from 13572c4 to 82112d7CompareJune 2, 2026 19:14
@denik
deniktemporarily deployed to test-trigger-is June 2, 2026 19:14 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 2, 2026 19:14 — with GitHub Actions Inactive
@denik
denikforce-pushed the denik/new-migration branch from 82112d7 to 969c201CompareJune 4, 2026 09:41
@denik
deniktemporarily deployed to test-trigger-is June 4, 2026 09:42 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 4, 2026 09:42 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 4, 2026 09:51 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 4, 2026 09:51 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 4, 2026 10:09 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 4, 2026 10:09 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 4, 2026 10:55 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 4, 2026 10:55 — with GitHub Actions Inactive
@denik
denikforce-pushed the denik/new-migration branch from d9d854f to f9471b6CompareJune 4, 2026 11:06
@denik
deniktemporarily deployed to test-trigger-is June 4, 2026 11:06 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 4, 2026 11:06 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 5, 2026 13:02 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 5, 2026 13:02 — with GitHub Actions Inactive
@denik
denikforce-pushed the denik/new-migration branch from fbe207c to 8a1ad59CompareJune 5, 2026 14:48
@denik
deniktemporarily deployed to test-trigger-is June 5, 2026 14:49 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 5, 2026 14:49 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 5, 2026 15:09 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 5, 2026 15:09 — with GitHub Actions Inactive
@denik
denikforce-pushed the denik/new-migration branch from 6cd9860 to c8f326fCompareJune 5, 2026 15:49
@denik
deniktemporarily deployed to test-trigger-is June 5, 2026 15:49 — with GitHub Actions Inactive
@denik
deniktemporarily deployed to test-trigger-is June 5, 2026 15:49 — with GitHub Actions Inactive
denik added 26 commits June 24, 2026 13:25
Both callers were reading and parsing the same .tfstate file twice —
once for resource IDs, once for full attributes. Now a single
ParseTFStateFull reads and unmarshals the file once, returning attrs,
IDs, and lineage/serial together.
Also drop the separate `etags map[string]string` parameter from
BuildStateFromTF. The dashboard etag is a regular attribute in the TF
state JSON ("etag" field), so LookupTFField finds it directly without
any special-case plumbing.
Implementation:
- terraform: expose ParseResourcesStateFromBytes so callers can pass
already-read bytes
- migrate: add ParseTFStateFull / parseTFStateAttrsFromBytes
- migrate: remove etags param; look up "etag" via LookupTFField
- tests: etag test now puts the etag in the TF attributes JSON
Co-authored-by: Isaac
Still useful as a standalone API; suppress the dead-code checker.
Co-authored-by: Isaac
Return (nil, nil, ...) when the state file doesn't exist, matching the
old parseResourcesState behaviour. Empty bundles with no resources
don't create a terraform.tfstate file.
Co-authored-by: Isaac
Remove the musterr + --noplancheck pattern; the plan check was removed
so migration succeeds on the first call. Regenerate output files.
Co-authored-by: Isaac
YAML + TF JSON as input, expected state fields as output.
Co-authored-by: Isaac
Replace the verbose LookupTFField call for etag with a direct JSON read
via TFStateAttrs.ETagFor(group, name).
Co-authored-by: Isaac
- Introduce rawTFState that captures lineage/serial alongside resources
in one unmarshal, eliminating the separate meta struct parse.
- parseTFStateAttrsFromBytes and parseTFStateAttrsFromRaw now share the
same struct instead of defining an anonymous one.
- Move Lineage/Serial after Attrs/IDs in TFState struct.
Co-authored-by: Isaac
DABsPathToTerraform now correctly handles root-level TF fields for
wrapped groups (e.g. postgres) via DABsToTerraformWrapperFields, so the
retry-with-unwrapped-path fallback is no longer needed.
Co-authored-by: Isaac
- Remove the unused `rest` variable in the alias resolution block
(SkipPrefix(1) on a single-segment path always returns nil)
- Remove ParseTFStateAttrs which has no callers; the deadcode:allow
annotation was papering over the linter
Co-authored-by: Isaac
BuildStateFromTF silently skips resources not in TF state (like
schema grants in terraform mode), so the old "state entry not found"
warning no longer fires. Remove the two spurious warning lines.
Co-authored-by: Isaac
The ETag field is no longer needed in the migration path — etags are
read directly from TFStateAttrs.ETagFor. Replace ExportedResourcesMap
with map[string]string throughout migrate, build_state, and callers.
Co-authored-by: Isaac
json.Unmarshal into map[string]any decodes all JSON numbers as float64.
Integers beyond 2^53 (~9e15) lose precision: 9007199254740993 becomes
9007199254740992. run_job_task.job_id is a JSON number in TF state and
realistic job IDs can exceed this threshold.
Fix: use json.NewDecoder.UseNumber() when parsing TF state attributes so
numbers are preserved as their original decimal string.
Add a unit test case with job_id = 2^53+1 that asserts the raw state JSON
contains the exact value, and an invariant config (job_run_job_ref.yml.tmpl)
that exercises the run_job_task.job_id cross-reference in cloud tests.
Co-authored-by: Isaac
Extend job_run_job_ref so watcher_job references trigger_job's
max_concurrent_runs literal of 2^53+1 (9007199254740993). This drives
the migration to resolve a large integer from TF state end-to-end; with
the json.Number fix the migrated state keeps the exact value, without it
the value is truncated to 2^53.
The value is out of range for the real backend, so the config is
local-only (no_run_job_ref_on_cloud).
Note: the unit test (TestBuildStateFromTF/large_integer) is the
deterministic regression guard, since the "no drift" plan check cannot
detect this — the plan diff also collapses ints to float64.
Co-authored-by: Isaac
main's DABsToTerraformRenameMap now maps models.model_id ->
registered_model_id (PR #5621), so the manual tfStateFieldAliases map
and the alias-fallback branch in LookupTFField are redundant.
LookupTFField now translates once and navigates.
Co-authored-by: Isaac
Group all type/struct declarations (TFStateAttrs, TFState, rawTFState) at
the top of the file, with methods and functions below.
Co-authored-by: Isaac
Version was never read (ParseResourcesStateFromBytes validates the state
version separately). Order the serial/lineage fields to match their
on-disk order in the terraform state file.
Co-authored-by: Isaac
ETagFor did its own json.Unmarshal of the attrs blob; the general
LookupTFField path already reads any TF-state field. Use it for "etag"
too (a resource without an etag returns an error, treated as "no etag"),
removing the one-off parser.
Co-authored-by: Isaac
Put Serial/Lineage at the top of TFState to match rawTFState's field
order (metadata before resource data).
Co-authored-by: Isaac
return valueA, nil
}
// Both succeeded but disagree: prefer longer string and warn.
if len(valueB) > len(aStr) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why longer value? What is the logic here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The failure to resolve would results in empty string.
  2. We want some deterministic selection there.

return "", fmt.Errorf("cannot look up %q: %w", pathString, err)
}

result = strings.ReplaceAll(result, "${"+pathString+"}", fmt.Sprintf("%v", value))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if value returned by LookupTFField is non-scalar, like a map[string]any ? Then stringified value might be incorrect because it will be a Go representation not a JSON string if I read this correctly

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is evaluateTemplate, so the result must be a string. Still, good question whether to use go or json representation, I'm not sure we have that defined anywhere.

if key, ok := node.StringKey(); ok {
// Auto-unwrap TF list-blocks: if the current value is a single-element
// array and the next step wants a map key, descend into element 0.
if arr, isArr := current.([]any); isArr {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we only auto-unwrap elements with MaxItems:1? Because oter can be genuinely repeated blocks (task, cluster, library) and they are valid multi-element arrays

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not know about MaxItems, is it part of tfschema?

We could check that but I'm not sure it adds value -- the goal here is to migrate whatever we have, not validate correctness of user references (and the case you describe can only happen for incorrect references like task.bla).

@eng-dev-ecosystem-bot

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: 2de2c79

Run: 28098701829

Env❌​FAIL🟨​KNOWN🔄​flaky💚​RECOVERED🙈​SKIP✅​pass🙈​skipTime
💚​aws linux71324410244:33
🟨​aws windows71324610228:50
💚​aws-ucws linux7133349404:59
💚​aws-ucws windows7133369385:44
🔄​azure windows211524710207:19
❌​azure-ucws linux121153369369:25
❌​azure-ucws windows24153369348:23
💚​gcp linux11524610244:22
💚​gcp windows11524810224:57
25 interesting tests: 13 SKIP, 7 KNOWN, 3 FAIL, 2 flaky
Test Nameaws linuxaws windowsaws-ucws linuxaws-ucws windowsazure windowsazure-ucws linuxazure-ucws windowsgcp linuxgcp windows
🟨​TestAccept💚​R🟨​K💚​R💚​R💚​R💚​R🔄​f💚​R💚​R
🔄​TestAccept/bundle/generate/alert✅​p✅​p✅​p✅​p✅​p✅​p🔄​f✅​p✅​p
🔄​TestAccept/bundle/generate/alert/DATABRICKS_BUNDLE_ENGINE=terraform✅​p✅​p✅​p✅​p✅​p✅​p🔄​f✅​p✅​p
🙈​TestAccept/bundle/invariant/no_drift🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/permissions🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🟨​TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions💚​R🟨​K💚​R💚​R🙈​S🙈​S🙈​S🙈​S🙈​S
🟨​TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=direct💚​R🟨​K💚​R💚​R
🟨​TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=terraform💚​R🟨​K💚​R💚​R
🟨​TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions💚​R🟨​K💚​R💚​R🙈​S🙈​S🙈​S🙈​S🙈​S
🟨​TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=direct💚​R🟨​K💚​R💚​R
🟨​TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=terraform💚​R🟨​K💚​R💚​R
🙈​TestAccept/bundle/resources/postgres_branches/basic🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/postgres_branches/recreate🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/postgres_branches/replace_existing🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/postgres_branches/update_protected🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/postgres_branches/without_branch_id🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/postgres_endpoints/basic🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/postgres_projects/update_display_name🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/synced_database_tables/basic🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/ssh/connection🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
❌​TestFetchRepositoryInfoAPI_FromRepo✅​p✅​p✅​p✅​p✅​p🔄​f❌​F✅​p✅​p
❌​TestFetchRepositoryInfoAPI_FromRepo/root✅​p✅​p✅​p✅​p🔄​f🔄​f❌​F✅​p✅​p
❌​TestFetchRepositoryInfoAPI_FromRepo/subdir✅​p✅​p✅​p✅​p🔄​f❌​F🔄​f✅​p✅​p
Top 3 slowest tests (at least 2 minutes):
durationenvtestname
3:16aws-ucws windowsTestAccept
3:15gcp windowsTestAccept
3:10azure windowsTestAccept

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@denik@eng-dev-ecosystem-bot@pietern@andrewnester