Skip to content

Commit d15b537

Browse files
bundle/direct: conditionally upgrade state version when opted into DMS
Normal deploys keep writing state_version 2. When the bundle opts into experimental.record_deployment_history, the deploy upgrades the local state to dmsStateVersion (3) and stamps a new CurrentDmsVersion header field, so future DMS breaking changes or minimum-CLI requirements can be gated on what the state was written with. migrateState now reads up to dmsStateVersion but still auto-migrates only up to the baseline currentStateVersion, so a v3 state is neither auto-applied nor downgraded. The upgrade is applied explicitly in deployCore, never as an automatic migration. Co-authored-by: Isaac
1 parent 30db14f commit d15b537

12 files changed

Lines changed: 205 additions & 13 deletions

File tree

acceptance/acceptance_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,13 @@ func testAccept(t *testing.T, inprocessMode bool, singleTest string) int {
277277
cli293Path := DownloadCLI(t, buildDir, "0.293.0")
278278
t.Setenv("CLI_293", cli293Path)
279279
repls.SetPath(cli293Path, "[CLI_293]")
280+
281+
// v1.0.0 understands state schema versions only up to 2. Used by tests
282+
// asserting that an older CLI rejects newer state (e.g. the v3 state
283+
// written when a bundle opts into the deployment metadata service).
284+
cliV1Path := DownloadCLI(t, buildDir, "1.0.0")
285+
t.Setenv("CLI_V1", cliV1Path)
286+
repls.SetPath(cliV1Path, "[CLI_V1]")
280287
}
281288

282289
paths := []string{
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
bundle:
2+
name: test-rdh-state-upgrade
3+
4+
experimental:
5+
record_deployment_history: true
6+
7+
resources:
8+
jobs:
9+
foo:
10+
name: foo-job-renamed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
bundle:
2+
name: test-rdh-state-upgrade
3+
4+
resources:
5+
jobs:
6+
foo:
7+
name: foo-job

acceptance/bundle/deploy/record-deployment-history/state-upgrade/out.test.toml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
=== without the flag: state stays at the baseline schema version
3+
>>> [CLI] bundle deploy
4+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-rdh-state-upgrade/default/files...
5+
Deploying resources...
6+
Updating deployment state...
7+
Deployment complete!
8+
9+
>>> print_state.py
10+
{
11+
"state_version": 2,
12+
"current_dms_version": null
13+
}
14+
15+
=== with experimental.record_deployment_history: state upgrades and records the DMS version
16+
>>> [CLI] bundle deploy
17+
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-rdh-state-upgrade/default/files...
18+
Deploying resources...
19+
Updating deployment state...
20+
Deployment complete!
21+
22+
>>> print_state.py
23+
{
24+
"state_version": 3,
25+
"current_dms_version": 1
26+
}
27+
28+
=== an older CLI (v1.0.0, max state version 2) rejects operations on the upgraded state
29+
>>> [CLI_V1] bundle plan
30+
Warning: unknown field: record_deployment_history
31+
at experimental
32+
in databricks.yml:5:3
33+
34+
Error: migrating state [TEST_TMP_DIR]/.databricks/bundle/default/resources.json: state version 3 is newer than supported version 2; upgrade the CLI
35+
36+
37+
Exit code: 1
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# State-version lifecycle for the DMS preview (experimental.record_deployment_history):
2+
# 1. Without the flag, deploys keep writing the baseline state version.
3+
# 2. Opting in upgrades the state version and records the DMS version.
4+
# 3. An older CLI that predates the DMS state version refuses to operate on the
5+
# upgraded state and tells the user to upgrade, so it cannot mishandle it.
6+
7+
title "without the flag: state stays at the baseline schema version"
8+
trace $CLI bundle deploy
9+
trace print_state.py | jq '{state_version, current_dms_version}'
10+
11+
title "with experimental.record_deployment_history: state upgrades and records the DMS version"
12+
# Also renames the job so the deploy writes state (a no-op deploy would not
13+
# rewrite the state file and the upgrade would not be observable yet).
14+
cp databricks.dms.yml databricks.yml
15+
trace $CLI bundle deploy
16+
trace print_state.py | jq '{state_version, current_dms_version}'
17+
18+
title "an older CLI (v1.0.0, max state version 2) rejects operations on the upgraded state"
19+
trace $CLI_V1 bundle plan 2>&1 | contains.py "state version 3 is newer than supported version 2; upgrade the CLI"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Local = true
2+
Cloud = false
3+
4+
# databricks.yml is rewritten in-place by the script (flag added in the second step).
5+
Ignore = [".databricks", "databricks.yml"]
6+
7+
# The state-version upgrade only applies to the direct engine; terraform stores
8+
# state differently and would diverge.
9+
[EnvMatrix]
10+
DATABRICKS_BUNDLE_ENGINE = ["direct"]

bundle/direct/dstate/migrate.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,22 @@ import (
1010
"github.com/databricks/databricks-sdk-go/service/iam"
1111
)
1212

13-
// migrateState runs all necessary migrations on the database.
13+
// migrateState brings a freshly-loaded state up to a version this CLI can use.
1414
// It is called after loading state from disk.
15+
//
16+
// Two versions are "current" and left untouched: the baseline currentStateVersion
17+
// and the opt-in dmsStateVersion (written when a bundle previews DMS). Legacy
18+
// states below the baseline are migrated forward via the migrations map. A state
19+
// newer than dmsStateVersion was written by a newer CLI, so we refuse it rather
20+
// than risk mishandling a format we don't understand.
1521
func migrateState(db *Database) error {
16-
if db.StateVersion == currentStateVersion {
17-
return nil
18-
}
19-
if db.StateVersion > currentStateVersion {
20-
return fmt.Errorf("state version %d is newer than supported version %d; upgrade the CLI", db.StateVersion, currentStateVersion)
22+
if db.StateVersion > dmsStateVersion {
23+
return fmt.Errorf("state version %d is newer than supported version %d; upgrade the CLI", db.StateVersion, dmsStateVersion)
2124
}
2225

26+
// Only legacy states (below the baseline) migrate here. The DMS upgrade is an
27+
// explicit deploy-time step (see UpgradeToDMS), never an automatic migration,
28+
// so a dmsStateVersion state falls through this loop unchanged.
2329
for version := db.StateVersion; version < currentStateVersion; version++ {
2430
fn, ok := migrations[version]
2531
if !ok {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package dstate
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestMigrateStateLeavesCurrentUntouched(t *testing.T) {
11+
db := &Database{Header: Header{StateVersion: currentStateVersion}}
12+
require.NoError(t, migrateState(db))
13+
assert.Equal(t, currentStateVersion, db.StateVersion)
14+
}
15+
16+
func TestMigrateStateLeavesDMSStateUntouched(t *testing.T) {
17+
// A DMS-upgraded state is already current; it must not be downgraded to the
18+
// baseline version, and the recorded DMS version must be preserved.
19+
db := &Database{Header: Header{StateVersion: dmsStateVersion, CurrentDmsVersion: currentDmsVersion}}
20+
require.NoError(t, migrateState(db))
21+
assert.Equal(t, dmsStateVersion, db.StateVersion)
22+
assert.Equal(t, currentDmsVersion, db.CurrentDmsVersion)
23+
}
24+
25+
func TestMigrateStateRejectsNewerThanSupported(t *testing.T) {
26+
db := &Database{Header: Header{StateVersion: dmsStateVersion + 1}}
27+
err := migrateState(db)
28+
require.Error(t, err)
29+
assert.Contains(t, err.Error(), "upgrade the CLI")
30+
}

bundle/direct/dstate/state.go

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,32 @@ import (
2121
)
2222

2323
const (
24+
// currentStateVersion is the schema version written for normal deployments
25+
// and the target that older states are migrated up to on load.
26+
//
27+
// NOTE: the next bump to the baseline schema must go to 4, not 3 (which is
28+
// reserved by dmsStateVersion below), and should delete dmsStateVersion,
29+
// folding DMS state handling back into normal versioning.
2430
currentStateVersion = 2
25-
initialBufferSize = 64 * 1024
26-
maxWalEntrySize = 10 * 1024 * 1024
27-
walSuffix = ".wal"
31+
32+
// dmsStateVersion is the schema version written when the bundle opts into the
33+
// deployment metadata service via experimental.record_deployment_history. It
34+
// is also the newest version this CLI understands; newer states are rejected.
35+
//
36+
// It is kept separate from currentStateVersion on purpose: previewing DMS
37+
// must not force a state upgrade on everyone else. Non-DMS deploys stay at
38+
// currentStateVersion while only DMS opt-in bumps the state to this version.
39+
// Remove it once currentStateVersion is bumped (to 4) and the two reconcile.
40+
dmsStateVersion = 3
41+
42+
// currentDmsVersion is stamped into DMS-upgraded state. Bump it when the DMS
43+
// protocol changes in a way the CLI must record (e.g. a breaking change or a
44+
// new minimum CLI version) so future deploys can gate behavior on it.
45+
currentDmsVersion = 1
46+
47+
initialBufferSize = 64 * 1024
48+
maxWalEntrySize = 10 * 1024 * 1024
49+
walSuffix = ".wal"
2850
)
2951

3052
// errStaleWAL is returned when the WAL serial is behind the expected serial.
@@ -42,10 +64,16 @@ type DeploymentState struct {
4264
}
4365

4466
type Header struct {
45-
StateVersion int `json:"state_version"`
46-
CLIVersion string `json:"cli_version"`
47-
Lineage string `json:"lineage"`
48-
Serial int `json:"serial"`
67+
StateVersion int `json:"state_version"`
68+
69+
// CurrentDmsVersion records the deployment metadata service (DMS) protocol
70+
// version this state was written with. Set only for states opted into DMS
71+
// (see dmsStateVersion) and omitted otherwise.
72+
CurrentDmsVersion int `json:"current_dms_version,omitempty"`
73+
74+
CLIVersion string `json:"cli_version"`
75+
Lineage string `json:"lineage"`
76+
Serial int `json:"serial"`
4977
}
5078

5179
type Database struct {
@@ -414,6 +442,17 @@ func (db *DeploymentState) UpgradeToWrite() error {
414442
return appendJSONLine(db.walFile, walHead)
415443
}
416444

445+
// UpgradeToDMS marks the state as opted into the deployment metadata service
446+
// (DMS): it bumps the schema to dmsStateVersion and stamps the current DMS
447+
// version. The change is persisted on the next save. State must be open for write.
448+
func (db *DeploymentState) UpgradeToDMS() {
449+
db.AssertOpenedForWrite()
450+
db.mu.Lock()
451+
defer db.mu.Unlock()
452+
db.Data.StateVersion = dmsStateVersion
453+
db.Data.CurrentDmsVersion = currentDmsVersion
454+
}
455+
417456
func (db *DeploymentState) AssertOpenedForReadOrWrite() {
418457
if db.Path == "" {
419458
panic("internal error: DeploymentState must be opened first")

0 commit comments

Comments
 (0)