Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
167 commits
Select commit Hold shift + click to select a range
f0673ef
bundle: record and read deployment state via DMS
shreyas-goenka Sep 3, 2026
a9ce3a2
acceptance/dms: list-resources shows empty tombstone for no-resources
shreyas-goenka Sep 4, 2026
4abbea0
acceptance/dms: assert declined deploy leaves recorded resources unch…
shreyas-goenka Sep 4, 2026
2a56b8b
acceptance/dms: list-resources coverage for depends-on and emptied-re…
shreyas-goenka Sep 4, 2026
af3ffde
acceptance/dms: inline bundle summary into no-drift + declined-deploy…
shreyas-goenka Sep 4, 2026
0978bb8
libs/dms: document the three SDK gaps the raw client works around; dr…
shreyas-goenka Sep 4, 2026
6163285
acceptance/dms: fold serialized-plan header assertion into stale-plan…
shreyas-goenka Sep 4, 2026
632b8ea
acceptance/bin: centralize recorded-state reconstruction in dms_resou…
shreyas-goenka Sep 4, 2026
1b0b45f
acceptance/dms: record-failure uses a real bogus node type + a scoped…
shreyas-goenka Sep 4, 2026
09f61c3
libs/dms: gofmt the rawClient doc comment
shreyas-goenka Sep 4, 2026
d42b08e
bundle: make the recorded-state marker the source of truth for DMS; O…
shreyas-goenka Sep 4, 2026
8dc139d
bundle: move the DMS client and operation buffer into StateDB; drop t…
shreyas-goenka Sep 4, 2026
8ec86f8
bundle/dms: StorageBackend enum as source of truth; hide operation bu…
shreyas-goenka Sep 4, 2026
3683aed
bundle/dms: rename first-version stamp, inline env predicate, state-g…
shreyas-goenka Sep 4, 2026
c0bc24d
acceptance/dms: record plan JSON, list-resources, and raw recorded state
shreyas-goenka Sep 4, 2026
eaa31f4
acceptance/dms: record plan JSON, list-resources, and raw recorded st…
shreyas-goenka Sep 4, 2026
db6a166
bundle/dms: guard StorageBackend() gate on direct engine; bump future…
shreyas-goenka Sep 4, 2026
0afcb13
bundle/dms: StateDB owns plan lineage; unbind-specific message; fail …
shreyas-goenka Sep 4, 2026
0f92c14
acceptance/dms: drop dedicated readplan test; cover deploy --plan via…
shreyas-goenka Sep 4, 2026
5f10805
bundle/dms: trim redundant comment on the cmdctx workspace-client bridge
shreyas-goenka Sep 4, 2026
abd7357
acceptance/dms: record plan/list-resources/state for the failure tests
shreyas-goenka Sep 4, 2026
b4d23d2
bundle/dms: review fixes - naming, single lineage fields, user-facing…
shreyas-goenka Sep 4, 2026
97ba52a
hand edits
shreyas-goenka Sep 6, 2026
dad2993
bundle/dms: fix hand-edit regressions in unlockedOpen and declined-de…
shreyas-goenka Sep 6, 2026
bb5abcd
acceptance/dms: serialize deployment_id and version_id in plan output
shreyas-goenka Sep 6, 2026
aadf972
acceptance/dms: assert tombstone state via print_state.py --no-dms
shreyas-goenka Sep 6, 2026
5e7a846
acceptance/dms: assert tombstone state via print_state.py --no-dms; p…
shreyas-goenka Sep 6, 2026
ab79f3b
acceptance/dms: put record_deployment_history feature in existing-sta…
shreyas-goenka Sep 6, 2026
bbade26
acceptance/dms: drop two racy mid-flow titles in deployment-metadata-…
shreyas-goenka Sep 6, 2026
55b1256
acceptance/dms: drop redundant resources.json guard in print_state.py
shreyas-goenka Sep 7, 2026
41b1adf
acceptance/dms: assert failed delete records its error via list-opera…
shreyas-goenka Sep 7, 2026
4563cbc
bundle/direct/dstate: restore original migrateState feature-version c…
shreyas-goenka Sep 7, 2026
5435321
acceptance/dms: failed-recreate fails naturally via nonexistent catal…
shreyas-goenka Sep 7, 2026
ae90e56
bundle/direct/dstate: restore TestEmptyFeatureStateAcceptedWithoutFli…
shreyas-goenka Sep 7, 2026
6a21a0e
bundle/direct/dstate: use lock/defer-unlock in RecordingError
shreyas-goenka Sep 7, 2026
9806599
acceptance/dms: cover bind/unbind refusal while recording deployment …
shreyas-goenka Sep 7, 2026
8d65ab2
libs/dms: fold latestState update into record under one lock
shreyas-goenka Sep 7, 2026
c0a2e50
acceptance/dms: print plan header via jq + readable plan instead of f…
shreyas-goenka Sep 7, 2026
d76883e
libs/dms: shorten record/RecordFailure comments, rename flag to keepE…
shreyas-goenka Sep 7, 2026
f839765
acceptance/dms: bind test uses a real job so it records the DMS refusal
shreyas-goenka Sep 7, 2026
f8894d5
acceptance/dms: rename emptied-resource to empty-grants, record the k…
shreyas-goenka Sep 7, 2026
4b01fa8
acceptance/dms: assert failed permissions update records its error vi…
shreyas-goenka Sep 7, 2026
7bf906a
acceptance/dms: remove operation-upload-fails test
shreyas-goenka Sep 7, 2026
bdcc887
acceptance/dms: remove redundant record and failed-update tests
shreyas-goenka Sep 7, 2026
cc4d4cc
libs/testserver: gofmt fake_workspace.go after seeding the main catalog
shreyas-goenka Sep 7, 2026
983777e
Class A: Revert catalog-existence validation regression
shreyas-goenka Sep 7, 2026
ab2ccc6
Regenerate failed-recreate test golden after fault injection rewrite
shreyas-goenka Sep 7, 2026
7b84ffe
Add storage_backend to nostamp.py plan header keys
shreyas-goenka Sep 7, 2026
101fa48
Class B: Pipe bundle plan JSON through nostamp.py in 32 tests
shreyas-goenka Sep 7, 2026
ef650bc
Fix failed-recreate: ensure out.requests.txt cleanup and suppress ver…
shreyas-goenka Sep 7, 2026
a85053c
Regenerate acceptance test goldens after nostamp.py fixes
shreyas-goenka Sep 7, 2026
2488374
libs/testserver: gofmt struct alignment after reverting seeded main c…
shreyas-goenka Sep 7, 2026
eba427d
acceptance: strip leaked CLI_VERSION commit suffix from regenerated g…
shreyas-goenka Sep 7, 2026
818e972
acceptance: harden CLI_VERSION repl against +sha suffix; skip empty-g…
shreyas-goenka Sep 7, 2026
669fe1f
acceptance/dms: fix failed-recreate recreate trigger; skip readplan+D…
shreyas-goenka Sep 7, 2026
a42fcc3
Enable DMS testing for additional bundle tests with saved plans
shreyas-goenka Sep 7, 2026
d32d92e
Enable DMS testing for resources/models/readplan-permissions
shreyas-goenka Sep 7, 2026
ff8c67d
Enable DMS testing for dashboards/change-serialized-dashboard
shreyas-goenka Sep 7, 2026
0319c54
Enable DMS testing for more dashboard tests
shreyas-goenka Sep 7, 2026
20de1a7
Enable DMS testing for additional bundle tests
shreyas-goenka Sep 7, 2026
5d98f2b
Enable DMS testing for bundle/select tests
shreyas-goenka Sep 7, 2026
5863fda
Simplify plan saves: remove redundant tee+nostamp pattern
shreyas-goenka Sep 7, 2026
0f14dd4
Fix whitespace: remove trailing blank lines from test.toml files
shreyas-goenka Sep 7, 2026
620fcb9
Update golden outputs for DMS-enabled tests
shreyas-goenka Sep 7, 2026
15c7df2
Enable DMS for serialized dashboard test
shreyas-goenka Sep 7, 2026
9442cab
Enable DMS for dashboard tests: simple and detect-change
shreyas-goenka Sep 7, 2026
3dbda98
Enable DMS for more dashboard tests
shreyas-goenka Sep 7, 2026
0c743d9
Add nostamp wrapper function to script.prepare
shreyas-goenka Sep 7, 2026
d1e2801
Add explicit DMS field validation for serialized plans
shreyas-goenka Sep 7, 2026
294bcc8
Fix select tests to run with all DMS/READPLAN combinations
shreyas-goenka Sep 7, 2026
4834820
Update select tests to use engine-specific plan files
shreyas-goenka Sep 7, 2026
07dce69
Relax DMS plan validation to allow first-deploy plans
shreyas-goenka Sep 7, 2026
00c17ce
Update plan_file_survives_not_selected golden output
shreyas-goenka Sep 7, 2026
041013c
Set NextVersionId for first deployments in plan generation
shreyas-goenka Sep 7, 2026
94f0646
Add DMS test: plan without DMS rejected when target has DMS
shreyas-goenka Sep 7, 2026
a0730e2
Update golden file for plan-without-dms test
shreyas-goenka Sep 7, 2026
b5960ca
Simplify plan-without-dms test to use print_requests.py
shreyas-goenka Sep 7, 2026
4024655
Remove unnecessary plan corruption from test
shreyas-goenka Sep 7, 2026
6123585
acceptance/bundle/deploy/readplan: Enable DMS testing for grants-remo…
shreyas-goenka Sep 7, 2026
e767ed8
acceptance/bin: consolidate DMS helpers into print_state.py
shreyas-goenka Sep 7, 2026
bde2b0e
acceptance/bundle/deploy/readplan: Fix tests to work with DMS by usin…
shreyas-goenka Sep 7, 2026
b0ae7f2
acceptance/bundle/deploy/readplan: Regenerate outputs for fixed tests
shreyas-goenka Sep 7, 2026
633e357
acceptance/readplan: normalize recorded requests with nostamp under DMS
shreyas-goenka Sep 7, 2026
e55afd5
acceptance/readplan: nostamp recorded requests in cli-version-mismatch
shreyas-goenka Sep 7, 2026
aab6337
acceptance/bin: inline the deployment-history env check
shreyas-goenka Sep 7, 2026
d365f85
Merge remote-tracking branch 'origin/main' into isaac/pr6052-fixes
shreyas-goenka Sep 7, 2026
92c61c5
Revert first-deploy version stamping and the extra plan validation
shreyas-goenka Sep 7, 2026
37e5d6b
acceptance: match PLAN_VERSION repl to plan version 3; drop unused sy…
shreyas-goenka Sep 7, 2026
83c4fb8
acceptance/bin: document why get_id_recorded lists instead of GET
shreyas-goenka Sep 7, 2026
0e452f4
acceptance: stop nostamping plans that feed deploy --plan
shreyas-goenka Sep 7, 2026
63b2c7c
Use state features as the DMS signal in the plan, drop storage_backend
shreyas-goenka Sep 7, 2026
0a2d4bf
Say "bundle target" in the bind/unbind recording errors
shreyas-goenka Sep 7, 2026
2a9e879
acceptance/immutable: drop no-op nostamp
shreyas-goenka Sep 7, 2026
a7d1301
acceptance/wal: trim the DMS skip comment
shreyas-goenka Sep 7, 2026
b3eebb9
acceptance/select: add missing trailing newline (whitespace lint)
shreyas-goenka Sep 7, 2026
2bc157e
acceptance: regenerate goldens stale after the nostamp/plan-file changes
shreyas-goenka Sep 7, 2026
ed209b3
acceptance: stop nostamping plans captured with &> into a golden
shreyas-goenka Sep 7, 2026
a76b434
acceptance/bundle/resources/postgres_projects: Add compute_last_activ…
shreyas-goenka Sep 7, 2026
26f4329
acceptance/bundle/resources/grants/schemas/remove_all: Skip DMS=true …
shreyas-goenka Sep 7, 2026
62d461b
acceptance/bundle/resources/postgres_*: Add out.requests.txt to Ignor…
shreyas-goenka Sep 7, 2026
9cbedde
acceptance/bundle/resources/postgres_*: Remove --keep flag from print…
shreyas-goenka Sep 7, 2026
f6a0cfd
Revert "acceptance/bundle/resources/postgres_*: Remove --keep flag fr…
shreyas-goenka Sep 7, 2026
2d1eaa0
Revert "acceptance/bundle/resources/postgres_*: Add out.requests.txt …
shreyas-goenka Sep 7, 2026
94e4135
Revert "acceptance/bundle/resources/postgres_projects: Add compute_la…
shreyas-goenka Sep 7, 2026
d74c107
acceptance/grants: regenerate remove_all golden after adding test.toml
shreyas-goenka Sep 7, 2026
6138e60
Derive the deployment version from serial; no version for a no-op plan
shreyas-goenka Sep 8, 2026
f488c86
Rename record_deployment_history to deployment_history
shreyas-goenka Sep 8, 2026
f7ac997
bundle/phases: trim the version-creation comment
shreyas-goenka Sep 8, 2026
538852e
Validate serial against the deployment's last version; cover the stal…
shreyas-goenka Sep 8, 2026
26792ff
Reject a stale plan on serial vs the deployment's last version
shreyas-goenka Sep 8, 2026
243b3c4
Say "version" not "serial" in the stale-plan error
shreyas-goenka Sep 8, 2026
873109b
acceptance/apps: run readplan-lifecycle under DMS
shreyas-goenka Sep 8, 2026
7d1a57a
bundle/direct/dstate: set DeploymentID in Open
shreyas-goenka Sep 8, 2026
c345ea2
bundle/direct: co-locate the plan-vs-state validations
shreyas-goenka Sep 8, 2026
90b5c3b
bundle/deployplan: drop deployment_id from the plan, keep plan versio…
shreyas-goenka Sep 8, 2026
7750a32
acceptance: revert readplan plan fixtures to plan version 2
shreyas-goenka Sep 8, 2026
cf4f400
bundle/direct/dstate: take the serial from the deployment's last_vers…
shreyas-goenka Sep 8, 2026
16ef4b2
cmd/bundle/utils: validate lineage and serial for a plan that has nei…
shreyas-goenka Sep 8, 2026
bc0a598
bundle/direct/dstate: keep the created version on the state, not behi…
shreyas-goenka Sep 8, 2026
7033b09
bundle/phases: compute the update mask only where it is used
shreyas-goenka Sep 8, 2026
3aee9af
cmd/bundle/utils: note the state-path deployment lookup could be one API
shreyas-goenka Sep 8, 2026
2c91f6b
better comment
shreyas-goenka Sep 8, 2026
3767ff3
nit
shreyas-goenka Sep 8, 2026
c3b8e27
bundle/direct/dstate: write no WAL for a recorded deployment
shreyas-goenka Sep 8, 2026
e9dbe85
Merge remote-tracking branch 'origin/main' into isaac/pr6052-fixes
shreyas-goenka Sep 8, 2026
c3bd4ed
bundle/direct/dstate: tidy up the recorded write path
shreyas-goenka Sep 8, 2026
17ed80e
bundle/phases: read the deployment from the state, drop deploymentAnd…
shreyas-goenka Sep 8, 2026
a96c805
bundle/direct/dstate: make VersionID the recorded version, int all th…
shreyas-goenka Sep 8, 2026
77bd756
cmd/bundle/utils: check lineage before the version, set the deploymen…
shreyas-goenka Sep 8, 2026
f2c5ec6
cmd/bundle/utils: assign the fetched deployment to its own variables
shreyas-goenka Sep 8, 2026
94bf4ed
acceptance: restore serial-mismatch script
shreyas-goenka Sep 8, 2026
4776d57
acceptance/readplan: keep tmp plans out of the file count, run lineag…
shreyas-goenka Sep 8, 2026
73ca1d2
acceptance: run escaped_refs under recording, drop a needless nostamp…
shreyas-goenka Sep 8, 2026
d1aa9bd
acceptance: run two more jobs tests under recording, document the gra…
shreyas-goenka Sep 8, 2026
c73985d
Report the recorded version where the state serial used to be read
shreyas-goenka Sep 8, 2026
c7135c1
acceptance: run jobs/big_id under recording
shreyas-goenka Sep 8, 2026
ab50d9d
acceptance/telemetry: name the serial in the recording exclusion reason
shreyas-goenka Sep 8, 2026
a7e430a
acceptance/state_present: do not run it with recording
shreyas-goenka Sep 8, 2026
320cd34
acceptance: normalize the state serial in lineage-mismatch-after-rede…
shreyas-goenka Sep 8, 2026
9f03439
acceptance: say why the serial repl is needed
shreyas-goenka Sep 8, 2026
e1339e2
acceptance: resolve the deployment once, keep the lineage-mismatch fi…
shreyas-goenka Sep 8, 2026
324bd9d
Scrub the deployment stamp inside print_state.py, run two more tests …
shreyas-goenka Sep 8, 2026
fb1eb3c
acceptance/change_assets_dir: run it with recording
shreyas-goenka Sep 8, 2026
c05cc55
Merge remote-tracking branch 'origin/main' into isaac/pr6052-fixes
shreyas-goenka Sep 8, 2026
3aabe82
bundle/direct/dstate: keep recorded state in memory so metadata.json …
shreyas-goenka Sep 8, 2026
2e8baf1
acceptance: commit a DMS plan fixture, run two more tests with recording
shreyas-goenka Sep 9, 2026
8bcdc81
acceptance/destroy_without_mgmtperms: run it with recording
shreyas-goenka Sep 9, 2026
96ffaa2
acceptance/change_table_name: run it with recording
shreyas-goenka Sep 9, 2026
8f047e8
acceptance/lineage-mismatch: drop the redundant Ignore
shreyas-goenka Sep 9, 2026
8af91e8
bundle/direct/dstate: add GetSerial, always copy features into the plan
shreyas-goenka Sep 9, 2026
7a4424a
bundle/direct/dstate: GetSerial, refuse a leftover WAL, tidy DeleteState
shreyas-goenka Sep 9, 2026
fe3f963
acceptance/dms/existing-state: clear the stale WAL before destroy
shreyas-goenka Sep 9, 2026
0aa6c53
libs/dms: compare workspace info with structdiff.IsEqual, note the se…
shreyas-goenka Sep 9, 2026
3aac3ab
libs/dms: shorten the sameWorkspaceInfo doc comment
shreyas-goenka Sep 9, 2026
2467a05
Carry the deployment version as an int, add getOperationBuffer
shreyas-goenka Sep 9, 2026
7c6a157
bundle/direct/dstate: drop StorageBackend, read the features instead
shreyas-goenka Sep 9, 2026
5d35498
update comment
shreyas-goenka Sep 9, 2026
83f339e
better commentn
shreyas-goenka Sep 9, 2026
33be751
remove wrapper struct for deployment ID and last version ID
shreyas-goenka Sep 9, 2026
a4540bf
Regenerate the schema for the flattened deployment fields, complete t…
shreyas-goenka Sep 9, 2026
0dbffa2
bundle/direct/dstate: restore the empty-shell assertion in applyDMSState
shreyas-goenka Sep 9, 2026
93ac803
cmd/bundle/utils: note the half-created deployment recovery in the TODO
shreyas-goenka Sep 9, 2026
0883adc
libs/dms: record two deferred items in the code, correct the resource…
shreyas-goenka Sep 9, 2026
12e10e7
cmd/bundle/utils: split the two deployment-lookup TODOs
shreyas-goenka Sep 9, 2026
9233825
bundle/direct/dstate: cache whether the deployment records history
shreyas-goenka Sep 9, 2026
792146f
bundle/direct: move the plan-vs-state checks back into ValidatePlanAg…
shreyas-goenka Sep 9, 2026
ecf5608
acceptance/dms/successful-recreate: fix a title that claimed the wron…
shreyas-goenka Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 5 additions & 1 deletion acceptance/acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,10 @@ func testAccept(t *testing.T, inprocessMode bool, selectedTests []string, skipTo
if base, _, found := strings.Cut(cliVersion, "+"); found {
repls.Set(base, "[CLI_VERSION]")
}
// A dev build may embed a +<git-sha> that the base-version replacement above leaves
// behind (e.g. "[CLI_VERSION]+abc123def456"), which would otherwise bake into a
// regenerated golden. Strip any such trailing suffix so goldens stay sha-independent.
repls.Repls = append(repls.Repls, testdiff.Replacement{Old: regexp.MustCompile(`\[CLI_VERSION\]\+[0-9a-f]{7,40}`), New: "[CLI_VERSION]"})
testdiff.PrepareReplacementSdkVersion(t, &repls)
testdiff.PrepareReplacementTfProviderVersion(t, &repls)
testdiff.PrepareReplacementsGoVersion(t, &repls)
Expand Down Expand Up @@ -1078,7 +1082,7 @@ func checkEnvFilters(t *testing.T, testEnv, envFilters []string) {
// matrix key ends up in the variant's test name, so a long one makes every name that carries
// it hard to read. Tests may still name the variable itself; the alias is only shorter.
var envAliases = map[string]string{
"DMS": "DATABRICKS_BUNDLE_RECORD_DEPLOYMENT_HISTORY",
"DMS": "DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY",
}

// buildTestEnv builds the test environment from config.Env and customEnv.
Expand Down
41 changes: 0 additions & 41 deletions acceptance/bin/nostamp

This file was deleted.

79 changes: 79 additions & 0 deletions acceptance/bin/nostamp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Read JSON on stdin, write it back with the DMS deployment stamp removed.

Deployment history recording (DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY=true; see
bundle/test.toml) adds a deployment stamp to plans, states, and resource payloads. Pipe any of
those through this so an acceptance golden compares equal whether or not recording is on. Output
is a 2-space indent, keys in input order, <>& left unescaped, and integers at full precision.
Tests under bundle/dms assert the stamp itself and must not use it.

Removed in three shapes plus the plan header:
1. deployment_id/version_id nested in a deployment block, recognized by the
neighbouring "kind" and "metadata_file_path", which are kept; version_id
== "" is kept (a Terraform state dump carries that for an unstamped job).
2. changes entries keyed "deployment.deployment_id" / "deployment.version_id".
3. a changes object emptied by (2), or already empty.
4. the recorded plan header field features.
"""

import argparse
import json
import sys

_STAMP_KEYS = ("deployment_id", "version_id")
_CHANGE_KEYS = ("deployment.deployment_id", "deployment.version_id")
_PLAN_HEADER_KEYS = ("features",)


def scrub(node):
if isinstance(node, dict):
# 1. A deployment block is the only object carrying both of these.
if "kind" in node and "metadata_file_path" in node:
for k in _STAMP_KEYS:
if node.get(k, "") != "":
node.pop(k, None)
out = {}
for k, v in node.items():
if k == "changes" and isinstance(v, dict):
v = {ck: scrub(cv) for ck, cv in v.items() if ck not in _CHANGE_KEYS}
if not v:
continue # 3. drop a changes object left (or already) empty
out[k] = v
else:
out[k] = scrub(v)
return out
if isinstance(node, list):
return [scrub(x) for x in node]
return node


def render(data, indent):
data = scrub(data)
if isinstance(data, dict):
for k in _PLAN_HEADER_KEYS:
data.pop(k, None) # 4. plan header, present only at the root
return json.dumps(data, indent=indent, ensure_ascii=False, separators=(",", ": "))


def main():
parser = argparse.ArgumentParser()
# A state dump is printed with a single-space indent; plans use two.
parser.add_argument("--indent", type=int, default=2)
args = parser.parse_args()

# Input is one JSON value (a plan or state dump) or a whitespace-separated stream of them (each
# request emitted by a print_requests filter) - jq accepted both here, so this must too.
text = sys.stdin.read()
decoder = json.JSONDecoder()
idx, n = 0, len(text)
while idx < n:
while idx < n and text[idx].isspace():
idx += 1
if idx >= n:
break
data, idx = decoder.raw_decode(text, idx)
sys.stdout.write(render(data, args.indent) + "\n")


if __name__ == "__main__":
main()
169 changes: 167 additions & 2 deletions acceptance/bin/print_state.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
#!/usr/bin/env python3
"""
Print resources state from default target.
Print resources state from default target, and handle DMS (deployment metadata service) interactions.

Note, this intentionally has no logic on guessing what is the right state file (e.g. via DATABRICKS_BUNDLE_ENGINE),
the goal is to record all states that are available.
"""

import argparse
import functools
import glob
import json
import os
import posixpath
import subprocess

from nostamp import scrub


def print_file(filename):
Expand Down Expand Up @@ -53,14 +59,173 @@ def get_state_file(target, backup):
return filtered[0] if filtered else result[0]


# DMS (deployment metadata service) interaction helpers
# https://go.databricks.com/dms-service

CLI = os.environ.get("CLI", "databricks")
DEPLOYMENT_NODE_NAME = "resources.deployment.json" # Must match dms.DeploymentNodeName


def run_json(cmd, allow_failure=False):
"""Run cmd and parse its stdout, or return None if it fails and allow_failure is set.
stderr is captured rather than inherited: these lookups are plumbing, and a CLI warning
like "no files to sync" would otherwise land in the test output."""
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8")
if result.returncode != 0:
if allow_failure:
return None
raise SystemExit(f"{cmd} failed with code {result.returncode}\n{result.stdout}{result.stderr}".strip())
return json.loads(result.stdout)


def get_remote_state_path(target):
"""The bundle's remote state directory.

Preferred source is the sync snapshot, because it needs no CLI call: re-running the config
load would need whatever --var and flags the test deployed with, which a helper cannot know.
A bundle with no files to sync writes no snapshot, so fall back to asking the CLI - those
bundles are the ones with nothing to parameterize."""
target_dir = os.path.dirname(get_state_file(target, False))
snapshots = glob.glob(f"{target_dir}/sync-snapshots/*.json")
if snapshots:
# One snapshot per remote path, so a test that moved its root leaves several: the newest
# is the one the last deploy used.
newest = max(snapshots, key=os.path.getmtime)
remote_path = json.loads(open(newest).read())["remote_path"]
# state and files are siblings under the bundle root.
return posixpath.join(posixpath.dirname(remote_path), "state")

args = [CLI, "bundle", "validate", "--output", "json"]
if target:
args += ["-t", target]
return run_json(args)["workspace"]["state_path"]


@functools.cache
def get_deployment_id(target):
"""The recorded deployment's id, or None when nothing is recorded.

Cached and shared: both the resource listing and the version lookup need it, and resolving it
costs a workspace round trip. No node means nothing has been recorded, the conclusion
dms.resolveDeploymentID also draws from a 404 - the deployment is gone once the bundle is
destroyed.
"""
state_path = get_remote_state_path(target)
if not state_path:
return None
node = run_json([CLI, "workspace", "get-status", f"{state_path}/{DEPLOYMENT_NODE_NAME}"], allow_failure=True)
if not node or not node.get("object_id"):
return None
return node["object_id"]


@functools.cache
def get_resources(target):
"""Map every recorded resource key ("jobs.foo") to its {"id", "state"}.

Empty when the bundle has no deployment recorded yet. Cached because a lookup costs three
round trips and a script asks for one resource at a time.
"""
deployment_id = get_deployment_id(target)
if not deployment_id:
return {}

result = {}
# The service pages at 50 resources; the local fake returns everything at once.
page_token = None
while True:
url = f"/api/2.0/bundle/deployments/{deployment_id}/resources"
if page_token:
url += f"?page_token={page_token}"
listed = run_json([CLI, "api", "get", url])
for resource in listed.get("resources") or []:
# The service stores state as the opaque envelope the CLI wrote (dstate.RecordedState),
# so unwrap it to the resource state itself.
envelope = json.loads(resource["state"]) if resource.get("state") else {}
result[resource["resource_key"]] = {
"id": resource.get("resource_id"),
"state": envelope.get("state") or {},
"depends_on": envelope.get("depends_on") or [],
}
page_token = listed.get("next_page_token")
if not page_token:
return result


def get_recorded_state(target):
"""The recorded resources in the on-disk state file's `state` shape (resources.<key> ->
{__id__, state, depends_on}), so a recording run prints the same shape as a non-recording one."""
state = {}
for key, value in sorted(get_resources(target).items()):
entry = {"__id__": value["id"], "state": value["state"]}
if value["depends_on"]:
entry["depends_on"] = value["depends_on"]
state[f"resources.{key}"] = entry
return state


def get_last_version_id(target):
"""The version the service has recorded for this deployment, or None when nothing is recorded."""
deployment_id = get_deployment_id(target)
if not deployment_id:
return None
deployment = run_json([CLI, "api", "get", f"/api/2.0/bundle/deployments/{deployment_id}"])
return (deployment or {}).get("last_version_id")


def print_recorded_state(filename, target):
"""Print the state file with its resources filled in from the deployment metadata service.

While recording, the file itself carries only the header - the service holds the resources - so
printing it raw would show an empty state and differ from the same test's non-recording run.
"""
data = json.loads(open(filename).read())
# Recording stamps each resource payload with the deployment and version; drop it here so the
# printed state matches a non-recording run without every caller piping through nostamp.
data["state"] = scrub(get_recorded_state(target))

# The service owns the version and the file persists no serial, so take it from the deployment.
# Rebuilt in header order, since the file has no serial key to overwrite in place.
last_version_id = get_last_version_id(target)
serial = int(last_version_id) if last_version_id else 0

# Recording itself is not what these tests assert, so drop the feature that marks it.
features = {k: v for k, v in (data.get("features") or {}).items() if k != "deployment_history"}

rebuilt = {}
for key in ("state_version", "cli_version", "lineage"):
if key in data:
rebuilt[key] = data[key]
rebuilt["serial"] = serial
if features:
rebuilt["features"] = features
for key, value in data.items():
if key not in rebuilt and key != "features":
rebuilt[key] = value
print(json.dumps(rebuilt, indent=1))


def main():
parser = argparse.ArgumentParser()
parser.add_argument("-t", "--target")
parser.add_argument("--backup", action="store_true")
parser.add_argument(
"--no-dms",
action="store_true",
help="Print resources.json as-is without fetching state from DMS. "
"Use in bundle/dms tests to assert the tombstone content of the file.",
)
args = parser.parse_args()

for filename in get_state_files(args.target, args.backup):
if os.path.exists(filename):
if not os.path.exists(filename):
continue
# Recording only applies to the direct engine, so a terraform run prints the file as-is.
recording = os.environ.get("DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY") == "true"
terraform = os.environ.get("DATABRICKS_BUNDLE_ENGINE") == "terraform"
if recording and not terraform and not args.no_dms:
print_recorded_state(filename, args.target)
else:
print_file(filename)


Expand Down
31 changes: 26 additions & 5 deletions acceptance/bin/read_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@

import argparse
import json
import os
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
from add_repl import add_repl
from print_state import get_state_file
from print_state import get_resources, get_state_file


def get_id_terraform(filename, name):
Expand All @@ -34,6 +35,23 @@ def get_id_terraform(filename, name):
print(f"Cannot find resource with {name=}. Available: {available}", file=sys.stderr)


def get_id_recorded(target, name):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

read_id.py today only takes the job name without the resource type prefix (job1 vs jobs.job1). For now thus this is implemented via the list API and then only matching the suffix of the key.

Eventually we can simplify and make this more efficient by only relying on GET /resource as a followup.

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.

could you move this comment into the code? It's useful there.

I agree that this should be GET and that this can be a follow up. It's probably a sign that we might need to change database schema, since DABs currently enforce uniqueness of names and user-visible commands allow specifying just name without type prefix (bundle run my_job).

"""Find a recorded resource's id by its bare name ("foo"), scanning the resource list.

The GET resource API would be the direct lookup, but it needs the full resource key
("jobs.foo"), and callers pass only the leaf name - matching how user-visible commands
take a bare name ("bundle run my_job"), which DABs allows because it enforces name
uniqueness across types. So list and match on the leaf instead. Switching to GET is a
reasonable follow-up; it likely needs the service to key resources by name as well.
"""
resources = get_resources(target)
for key, value in resources.items():
if key.split(".")[1] == name:
return value["id"]

print(f"Cannot find recorded resource with {name=}. Available: {list(resources)}", file=sys.stderr)


def get_id_direct(filename, name):
raw = open(filename).read()
data = json.loads(raw)
Expand All @@ -53,11 +71,14 @@ def main():
parser.add_argument("name")
args = parser.parse_args()

filename = get_state_file(args.target, args.backup)
if filename.endswith(".tfstate"):
id = get_id_terraform(filename, args.name)
if os.environ.get("DATABRICKS_BUNDLE_DEPLOYMENT_HISTORY") == "true":
id = get_id_recorded(args.target, args.name)
else:
id = get_id_direct(filename, args.name)
filename = get_state_file(args.target, args.backup)
if filename.endswith(".tfstate"):
id = get_id_terraform(filename, args.name)
else:
id = get_id_direct(filename, args.name)

if id:
print(id)
Expand Down
Loading
Loading