Skip to content

Commit 3a3613e

Browse files
cmd/bundle: read state from DMS when record_deployment_history is set
Wire the StateReader into the direct-engine read path. NewStateReader selects the DMS reader when experimental.record_deployment_history is enabled and a prior deployment exists (lineage recorded in resources.json), otherwise the local file reader. process.go replaces the direct StateDB.Open call with the selector, so plan/deploy/summary read resource state from the deployment metadata service under the flag. The deployment ID is the state lineage (matching the lock package), read from the local resources.json; with no lineage yet (first deploy) there is nothing in DMS, so the local file reader is used. Co-authored-by: Isaac
1 parent 2cb0b41 commit 3a3613e

3 files changed

Lines changed: 115 additions & 2 deletions

File tree

bundle/statemgmt/statereader.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ package statemgmt
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
8+
"io/fs"
9+
"os"
10+
"path/filepath"
711

12+
"github.com/databricks/cli/bundle"
813
"github.com/databricks/cli/bundle/direct/dstate"
914
sdkbundle "github.com/databricks/databricks-sdk-go/service/bundle"
1015
)
@@ -97,3 +102,45 @@ func fetchDeploymentState(ctx context.Context, client sdkbundle.BundleInterface,
97102
}
98103
return data, nil
99104
}
105+
106+
// NewStateReader selects the StateReader for the bundle: the DMS reader when
107+
// experimental.record_deployment_history is enabled and a prior deployment
108+
// exists, otherwise the local resources.json reader.
109+
//
110+
// The DMS deployment ID is the state lineage, which is recorded in the local
111+
// resources.json (see dstate.DeploymentState.GetOrInitLineage). When there is no
112+
// lineage yet — a first deploy that has not registered with DMS — there is
113+
// nothing to read from the service, so we read the (empty) local file instead.
114+
func NewStateReader(ctx context.Context, b *bundle.Bundle, path string) (StateReader, error) {
115+
if b.Config.Experimental == nil || !b.Config.Experimental.RecordDeploymentHistory {
116+
return NewFileStateReader(path), nil
117+
}
118+
119+
lineage, err := readLineage(path)
120+
if err != nil {
121+
return nil, err
122+
}
123+
if lineage == "" {
124+
return NewFileStateReader(path), nil
125+
}
126+
127+
return NewDMSStateReader(b.WorkspaceClient(ctx).Bundle, lineage, path), nil
128+
}
129+
130+
// readLineage reads the deployment lineage from the local resources.json state
131+
// file. It returns an empty string when the file does not exist yet.
132+
func readLineage(path string) (string, error) {
133+
content, err := os.ReadFile(path)
134+
if errors.Is(err, fs.ErrNotExist) {
135+
return "", nil
136+
}
137+
if err != nil {
138+
return "", err
139+
}
140+
141+
var db dstate.Database
142+
if err := json.Unmarshal(content, &db); err != nil {
143+
return "", fmt.Errorf("parsing %s: %w", filepath.ToSlash(path), err)
144+
}
145+
return db.Lineage, nil
146+
}

bundle/statemgmt/statereader_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"path/filepath"
99
"testing"
1010

11+
"github.com/databricks/cli/bundle"
12+
"github.com/databricks/cli/bundle/config"
1113
"github.com/databricks/cli/bundle/direct/dstate"
1214
sdkbundle "github.com/databricks/databricks-sdk-go/service/bundle"
1315
"github.com/stretchr/testify/assert"
@@ -130,3 +132,63 @@ func TestFileStateReaderMissingFileIsEmptyState(t *testing.T) {
130132
_, ok := db.GetResourceEntry("resources.jobs.foo")
131133
assert.False(t, ok)
132134
}
135+
136+
func writeStateFile(t *testing.T, lineage string) string {
137+
t.Helper()
138+
path := filepath.Join(t.TempDir(), "resources.json")
139+
content, err := json.Marshal(dstate.NewDatabase(lineage, 1))
140+
require.NoError(t, err)
141+
require.NoError(t, os.WriteFile(path, content, 0o600))
142+
return path
143+
}
144+
145+
func TestReadLineage(t *testing.T) {
146+
t.Run("present", func(t *testing.T) {
147+
lineage, err := readLineage(writeStateFile(t, "lineage-9"))
148+
require.NoError(t, err)
149+
assert.Equal(t, "lineage-9", lineage)
150+
})
151+
152+
t.Run("missing file", func(t *testing.T) {
153+
lineage, err := readLineage(filepath.Join(t.TempDir(), "absent.json"))
154+
require.NoError(t, err)
155+
assert.Empty(t, lineage)
156+
})
157+
158+
t.Run("corrupt file", func(t *testing.T) {
159+
path := filepath.Join(t.TempDir(), "resources.json")
160+
require.NoError(t, os.WriteFile(path, []byte("not json"), 0o600))
161+
_, err := readLineage(path)
162+
assert.Error(t, err)
163+
})
164+
}
165+
166+
func TestNewStateReaderSelectsFileReader(t *testing.T) {
167+
// Flag disabled: always the file reader, regardless of any recorded lineage.
168+
b := &bundle.Bundle{}
169+
reader, err := NewStateReader(t.Context(), b, writeStateFile(t, "lineage-1"))
170+
require.NoError(t, err)
171+
assert.IsType(t, &fileStateReader{}, reader)
172+
}
173+
174+
func TestNewStateReaderFallsBackToFileWhenNoLineage(t *testing.T) {
175+
// Flag enabled but no prior deployment (no lineage yet): nothing in DMS to
176+
// read, so fall back to the local file reader.
177+
b := &bundle.Bundle{}
178+
b.Config.Experimental = &config.Experimental{RecordDeploymentHistory: true}
179+
180+
reader, err := NewStateReader(t.Context(), b, writeStateFile(t, ""))
181+
require.NoError(t, err)
182+
assert.IsType(t, &fileStateReader{}, reader)
183+
}
184+
185+
func TestNewStateReaderPropagatesCorruptStateError(t *testing.T) {
186+
b := &bundle.Bundle{}
187+
b.Config.Experimental = &config.Experimental{RecordDeploymentHistory: true}
188+
189+
path := filepath.Join(t.TempDir(), "resources.json")
190+
require.NoError(t, os.WriteFile(path, []byte("not json"), 0o600))
191+
192+
_, err := NewStateReader(t.Context(), b, path)
193+
assert.Error(t, err)
194+
}

cmd/bundle/utils/process.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"github.com/databricks/cli/bundle/deploy/terraform"
1515
"github.com/databricks/cli/bundle/deployplan"
1616
"github.com/databricks/cli/bundle/direct"
17-
"github.com/databricks/cli/bundle/direct/dstate"
1817
"github.com/databricks/cli/bundle/phases"
1918
"github.com/databricks/cli/bundle/statemgmt"
2019
"github.com/databricks/cli/cmd/root"
@@ -189,7 +188,12 @@ func ProcessBundleRet(cmd *cobra.Command, opts ProcessOptions) (b *bundle.Bundle
189188
needDirectState := stateDesc.Engine.IsDirect() && (opts.InitIDs || opts.ErrorOnEmptyState || opts.Deploy || opts.ReadPlanPath != "" || opts.PreDeployChecks || opts.PostStateFunc != nil)
190189
if needDirectState {
191190
_, localPath := b.StateFilenameDirect(ctx)
192-
if err := b.DeploymentBundle.StateDB.Open(ctx, localPath, dstate.WithRecovery(true), dstate.WithWrite(false)); err != nil {
191+
reader, err := statemgmt.NewStateReader(ctx, b, localPath)
192+
if err != nil {
193+
logdiag.LogError(ctx, err)
194+
return b, stateDesc, root.ErrAlreadyPrinted
195+
}
196+
if err := reader.Load(ctx, &b.DeploymentBundle.StateDB); err != nil {
193197
logdiag.LogError(ctx, err)
194198
return b, stateDesc, root.ErrAlreadyPrinted
195199
}

0 commit comments

Comments
 (0)