Skip to content

Commit 5c068bd

Browse files
fixed deployment failing when the app in in deleting state
1 parent b8b8cad commit 5c068bd

File tree

3 files changed

+207
-0
lines changed

3 files changed

+207
-0
lines changed

bundle/deploy/prepare.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package deploy
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/databricks/cli/bundle"
9+
"github.com/databricks/cli/libs/diag"
10+
"github.com/databricks/cli/libs/log"
11+
"github.com/databricks/databricks-sdk-go/apierr"
12+
"github.com/databricks/databricks-sdk-go/retries"
13+
"github.com/databricks/databricks-sdk-go/service/apps"
14+
)
15+
16+
type prepareEnvironment struct{}
17+
18+
func (p *prepareEnvironment) Name() string {
19+
return "deploy:prepare-environment"
20+
}
21+
22+
// Apply runs all pre-deployment environment preparation steps.
23+
func (p *prepareEnvironment) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
24+
// Check for resources in transient states and wait for them to complete.
25+
// This prevents deployment failures due to resources still being deleted.
26+
if err := waitForAppsDeletion(ctx, b); err != nil {
27+
return diag.FromErr(err)
28+
}
29+
30+
return nil
31+
}
32+
33+
// PrepareEnvironment returns a mutator that prepares the environment for deployment.
34+
// It runs checks and waits for resources in transient states before deployment proceeds.
35+
func PrepareEnvironment() bundle.Mutator {
36+
return &prepareEnvironment{}
37+
}
38+
39+
// waitForAppsDeletion waits for apps to be deleted if they are in DELETING state.
40+
func waitForAppsDeletion(ctx context.Context, b *bundle.Bundle) error {
41+
if len(b.Config.Resources.Apps) == 0 {
42+
return nil
43+
}
44+
45+
w := b.WorkspaceClient()
46+
47+
for _, app := range b.Config.Resources.Apps {
48+
appName := app.Name
49+
if appName == "" {
50+
continue
51+
}
52+
53+
log.Debugf(ctx, "Checking status of app %s", appName)
54+
55+
_, err := retries.Poll(ctx, 5*time.Minute, func() (*struct{}, *retries.Err) {
56+
appStatus, err := w.Apps.GetByName(ctx, appName)
57+
if err != nil {
58+
if apierr.IsMissing(err) {
59+
return nil, nil
60+
}
61+
return nil, retries.Halt(err)
62+
}
63+
64+
if appStatus.ComputeStatus.State == apps.ComputeStateDeleting {
65+
log.Infof(ctx, "App %s is in DELETING state, waiting for it to be deleted...", appName)
66+
return nil, retries.Continues("app is deleting")
67+
}
68+
69+
return nil, nil
70+
})
71+
if err != nil {
72+
return fmt.Errorf("failed to wait for app %s deletion: %w", appName, err)
73+
}
74+
}
75+
76+
return nil
77+
}

bundle/deploy/prepare_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package deploy
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/databricks/cli/bundle"
8+
"github.com/databricks/cli/bundle/config"
9+
"github.com/databricks/cli/bundle/config/resources"
10+
"github.com/databricks/databricks-sdk-go/apierr"
11+
"github.com/databricks/databricks-sdk-go/experimental/mocks"
12+
"github.com/databricks/databricks-sdk-go/service/apps"
13+
"github.com/stretchr/testify/mock"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestWaitForAppsDeletion(t *testing.T) {
18+
ctx := context.Background()
19+
b := &bundle.Bundle{
20+
Config: config.Root{
21+
Resources: config.Resources{
22+
Apps: map[string]*resources.App{
23+
"my_app": {
24+
App: apps.App{
25+
Name: "my_app",
26+
},
27+
},
28+
},
29+
},
30+
},
31+
}
32+
33+
mwc := mocks.NewMockWorkspaceClient(t)
34+
b.SetWorkpaceClient(mwc.WorkspaceClient)
35+
36+
appApi := mwc.GetMockAppsAPI()
37+
38+
appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(&apps.App{
39+
Name: "my_app",
40+
ComputeStatus: &apps.ComputeStatus{
41+
State: apps.ComputeStateDeleting,
42+
},
43+
}, nil).Once()
44+
45+
appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(nil, &apierr.APIError{
46+
StatusCode: 404,
47+
Message: "App not found",
48+
}).Once()
49+
50+
err := waitForAppsDeletion(ctx, b)
51+
require.NoError(t, err)
52+
}
53+
54+
func TestWaitForAppsDeletion_NoApps(t *testing.T) {
55+
ctx := context.Background()
56+
b := &bundle.Bundle{
57+
Config: config.Root{},
58+
}
59+
60+
err := waitForAppsDeletion(ctx, b)
61+
require.NoError(t, err)
62+
}
63+
64+
func TestWaitForAppsDeletion_AppNotDeleting(t *testing.T) {
65+
ctx := context.Background()
66+
b := &bundle.Bundle{
67+
Config: config.Root{
68+
Resources: config.Resources{
69+
Apps: map[string]*resources.App{
70+
"my_app": {
71+
App: apps.App{
72+
Name: "my_app",
73+
},
74+
},
75+
},
76+
},
77+
},
78+
}
79+
80+
mwc := mocks.NewMockWorkspaceClient(t)
81+
b.SetWorkpaceClient(mwc.WorkspaceClient)
82+
83+
appApi := mwc.GetMockAppsAPI()
84+
85+
appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(&apps.App{
86+
Name: "my_app",
87+
ComputeStatus: &apps.ComputeStatus{
88+
State: apps.ComputeStateActive,
89+
},
90+
}, nil).Once()
91+
92+
err := waitForAppsDeletion(ctx, b)
93+
require.NoError(t, err)
94+
}
95+
96+
func TestPrepareEnvironment(t *testing.T) {
97+
ctx := context.Background()
98+
b := &bundle.Bundle{
99+
Config: config.Root{
100+
Resources: config.Resources{
101+
Apps: map[string]*resources.App{
102+
"my_app": {
103+
App: apps.App{
104+
Name: "my_app",
105+
},
106+
},
107+
},
108+
},
109+
},
110+
}
111+
112+
mwc := mocks.NewMockWorkspaceClient(t)
113+
b.SetWorkpaceClient(mwc.WorkspaceClient)
114+
115+
appApi := mwc.GetMockAppsAPI()
116+
117+
appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(&apps.App{
118+
Name: "my_app",
119+
ComputeStatus: &apps.ComputeStatus{
120+
State: apps.ComputeStateActive,
121+
},
122+
}, nil).Once()
123+
124+
m := PrepareEnvironment()
125+
require.Equal(t, "deploy:prepare-environment", m.Name())
126+
127+
diags := m.Apply(ctx, b)
128+
require.Empty(t, diags)
129+
}

bundle/phases/deploy.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ func Deploy(ctx context.Context, b *bundle.Bundle, outputHandler sync.OutputHand
173173
permissions.ApplyWorkspaceRootPermissions(),
174174
metrics.TrackUsedCompute(),
175175
deploy.ResourcePathMkdir(),
176+
deploy.PrepareEnvironment(),
176177
)
177178

178179
if logdiag.HasError(ctx) {

0 commit comments

Comments
 (0)