Skip to content

Commit 0c28ffb

Browse files
Add remote modification check for dashboards
This change adds a mutator that checks if dashboards have been modified remotely since the last bundle deployment. It compares the ETag of the dashboard in the deployment state with the current ETag from the API. If a dashboard has been modified remotely, it: - Returns an error during deploy to prevent overwriting untracked changes - Returns a warning during plan to inform the user Users can bypass this check using `databricks bundle deploy --force`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 08cf9ee commit 0c28ffb

File tree

2 files changed

+56
-17
lines changed

2 files changed

+56
-17
lines changed

bundle/deploy/terraform/check_dashboards_modified_remotely.go

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,19 @@ type dashboardState struct {
1616
}
1717

1818
func collectDashboardsFromState(ctx context.Context, b *bundle.Bundle) ([]dashboardState, error) {
19-
state, err := ParseResourcesState(ctx, b)
20-
if err != nil && state == nil {
21-
return nil, err
19+
var state ExportedResourcesMap
20+
var err error
21+
if b.DirectDeployment {
22+
err := b.OpenStateFile(ctx)
23+
if err != nil {
24+
return nil, err
25+
}
26+
state = b.DeploymentBundle.StateDB.ExportState(ctx)
27+
} else {
28+
state, err = ParseResourcesState(ctx, b)
29+
if err != nil && state == nil {
30+
return nil, err
31+
}
2232
}
2333

2434
var dashboards []dashboardState
@@ -33,7 +43,9 @@ func collectDashboardsFromState(ctx context.Context, b *bundle.Bundle) ([]dashbo
3343
return dashboards, nil
3444
}
3545

36-
type checkDashboardsModifiedRemotely struct{}
46+
type checkDashboardsModifiedRemotely struct {
47+
isPlan bool
48+
}
3749

3850
func (l *checkDashboardsModifiedRemotely) Name() string {
3951
return "CheckDashboardsModifiedRemotely"
@@ -45,11 +57,6 @@ func (l *checkDashboardsModifiedRemotely) Apply(ctx context.Context, b *bundle.B
4557
return nil
4658
}
4759

48-
if b.DirectDeployment {
49-
// TODO: not implemented yet
50-
return nil
51-
}
52-
5360
// If the user has forced the deployment, skip this check.
5461
if b.Config.Bundle.Force {
5562
return nil
@@ -87,8 +94,14 @@ func (l *checkDashboardsModifiedRemotely) Apply(ctx context.Context, b *bundle.B
8794
continue
8895
}
8996

97+
// Downgrade this to a warning in plan mode.
98+
severity := diag.Error
99+
if l.isPlan {
100+
severity = diag.Warning
101+
}
102+
90103
diags = diags.Append(diag.Diagnostic{
91-
Severity: diag.Error,
104+
Severity: severity,
92105
Summary: fmt.Sprintf("dashboard %q has been modified remotely", dashboard.Name),
93106
Detail: "" +
94107
"This dashboard has been modified remotely since the last bundle deployment.\n" +
@@ -107,6 +120,6 @@ func (l *checkDashboardsModifiedRemotely) Apply(ctx context.Context, b *bundle.B
107120
return diags
108121
}
109122

110-
func CheckDashboardsModifiedRemotely() *checkDashboardsModifiedRemotely {
111-
return &checkDashboardsModifiedRemotely{}
123+
func CheckDashboardsModifiedRemotely(isPlan bool) *checkDashboardsModifiedRemotely {
124+
return &checkDashboardsModifiedRemotely{isPlan: isPlan}
112125
}

bundle/deploy/terraform/check_dashboards_modified_remotely_test.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@ func TestCheckDashboardsModifiedRemotely_NoDashboards(t *testing.T) {
5353
},
5454
}
5555

56-
diags := bundle.Apply(context.Background(), b, CheckDashboardsModifiedRemotely())
56+
diags := bundle.Apply(context.Background(), b, CheckDashboardsModifiedRemotely(false))
5757
assert.Empty(t, diags)
5858
}
5959

6060
func TestCheckDashboardsModifiedRemotely_FirstDeployment(t *testing.T) {
6161
b := mockDashboardBundle(t)
62-
diags := bundle.Apply(context.Background(), b, CheckDashboardsModifiedRemotely())
62+
diags := bundle.Apply(context.Background(), b, CheckDashboardsModifiedRemotely(false))
6363
assert.Empty(t, diags)
6464
}
6565

@@ -82,7 +82,7 @@ func TestCheckDashboardsModifiedRemotely_ExistingStateNoChange(t *testing.T) {
8282
b.SetWorkpaceClient(m.WorkspaceClient)
8383

8484
// No changes, so no diags.
85-
diags := bundle.Apply(ctx, b, CheckDashboardsModifiedRemotely())
85+
diags := bundle.Apply(ctx, b, CheckDashboardsModifiedRemotely(false))
8686
assert.Empty(t, diags)
8787
}
8888

@@ -105,7 +105,7 @@ func TestCheckDashboardsModifiedRemotely_ExistingStateChange(t *testing.T) {
105105
b.SetWorkpaceClient(m.WorkspaceClient)
106106

107107
// The dashboard has changed, so expect an error.
108-
diags := bundle.Apply(ctx, b, CheckDashboardsModifiedRemotely())
108+
diags := bundle.Apply(ctx, b, CheckDashboardsModifiedRemotely(false))
109109
if assert.Len(t, diags, 1) {
110110
assert.Equal(t, diag.Error, diags[0].Severity)
111111
assert.Equal(t, `dashboard "dash1" has been modified remotely`, diags[0].Summary)
@@ -128,13 +128,39 @@ func TestCheckDashboardsModifiedRemotely_ExistingStateFailureToGet(t *testing.T)
128128
b.SetWorkpaceClient(m.WorkspaceClient)
129129

130130
// Unable to get the dashboard, so expect an error.
131-
diags := bundle.Apply(ctx, b, CheckDashboardsModifiedRemotely())
131+
diags := bundle.Apply(ctx, b, CheckDashboardsModifiedRemotely(false))
132132
if assert.Len(t, diags, 1) {
133133
assert.Equal(t, diag.Error, diags[0].Severity)
134134
assert.Equal(t, `failed to get dashboard "dash1"`, diags[0].Summary)
135135
}
136136
}
137137

138+
func TestCheckDashboardsModifiedRemotely_ExistingStateChangePlanMode(t *testing.T) {
139+
ctx := context.Background()
140+
141+
b := mockDashboardBundle(t)
142+
writeFakeDashboardState(t, ctx, b)
143+
144+
// Mock the call to the API.
145+
m := mocks.NewMockWorkspaceClient(t)
146+
dashboardsAPI := m.GetMockLakeviewAPI()
147+
dashboardsAPI.EXPECT().
148+
GetByDashboardId(mock.Anything, "id1").
149+
Return(&dashboards.Dashboard{
150+
DisplayName: "My Special Dashboard",
151+
Etag: "1234",
152+
}, nil).
153+
Once()
154+
b.SetWorkpaceClient(m.WorkspaceClient)
155+
156+
// The dashboard has changed, but in plan mode expect a warning instead of an error.
157+
diags := bundle.Apply(ctx, b, CheckDashboardsModifiedRemotely(true))
158+
if assert.Len(t, diags, 1) {
159+
assert.Equal(t, diag.Warning, diags[0].Severity)
160+
assert.Equal(t, `dashboard "dash1" has been modified remotely`, diags[0].Summary)
161+
}
162+
}
163+
138164
func writeFakeDashboardState(t *testing.T, ctx context.Context, b *bundle.Bundle) {
139165
path, err := b.StateLocalPath(ctx)
140166
require.NoError(t, err)

0 commit comments

Comments
 (0)