From b8b8cad6915acbe3eb8fc61918fb9d4b6b3706c8 Mon Sep 17 00:00:00 2001 From: Varun Deep Saini Date: Sat, 29 Nov 2025 00:44:46 +0530 Subject: [PATCH 1/6] fixed deployment failing when the app in in deleting state --- bundle/deploy/wait.go | 53 +++++++++++++++++++++ bundle/deploy/wait_test.go | 94 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 bundle/deploy/wait.go create mode 100644 bundle/deploy/wait_test.go diff --git a/bundle/deploy/wait.go b/bundle/deploy/wait.go new file mode 100644 index 0000000000..7244a39603 --- /dev/null +++ b/bundle/deploy/wait.go @@ -0,0 +1,53 @@ +package deploy + +import ( + "context" + "fmt" + "time" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/log" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/retries" + "github.com/databricks/databricks-sdk-go/service/apps" +) + +// WaitForAppDeletion waits for apps to be deleted if they are in DELETING state. +func WaitForAppDeletion(ctx context.Context, b *bundle.Bundle) error { + if len(b.Config.Resources.Apps) == 0 { + return nil + } + + w := b.WorkspaceClient() + + for _, app := range b.Config.Resources.Apps { + appName := app.Name + if appName == "" { + continue + } + + log.Debugf(ctx, "Checking status of app %s", appName) + + _, err := retries.Poll(ctx, 10*time.Minute, func() (*struct{}, *retries.Err) { + appStatus, err := w.Apps.GetByName(ctx, appName) + if err != nil { + if apierr.IsMissing(err) { + return nil, nil + } + return nil, retries.Halt(err) + } + + if appStatus.ComputeStatus.State == apps.ComputeStateDeleting { + log.Infof(ctx, "App %s is in DELETING state, waiting for it to be deleted...", appName) + return nil, retries.Continues("app is deleting") + } + + return nil, nil + }) + if err != nil { + return fmt.Errorf("failed to wait for app %s deletion: %w", appName, err) + } + } + + return nil +} diff --git a/bundle/deploy/wait_test.go b/bundle/deploy/wait_test.go new file mode 100644 index 0000000000..eb47912df2 --- /dev/null +++ b/bundle/deploy/wait_test.go @@ -0,0 +1,94 @@ +package deploy + +import ( + "context" + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/experimental/mocks" + "github.com/databricks/databricks-sdk-go/service/apps" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestWaitForAppDeletion(t *testing.T) { + ctx := context.Background() + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Apps: map[string]*resources.App{ + "my_app": { + App: apps.App{ + Name: "my_app", + }, + }, + }, + }, + }, + } + + mwc := mocks.NewMockWorkspaceClient(t) + b.SetWorkpaceClient(mwc.WorkspaceClient) + + appApi := mwc.GetMockAppsAPI() + + appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(&apps.App{ + Name: "my_app", + ComputeStatus: &apps.ComputeStatus{ + State: apps.ComputeStateDeleting, + }, + }, nil).Once() + + appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(nil, &apierr.APIError{ + StatusCode: 404, + Message: "App not found", + }).Once() + + err := WaitForAppDeletion(ctx, b) + require.NoError(t, err) +} + +func TestWaitForAppDeletion_NoApps(t *testing.T) { + ctx := context.Background() + b := &bundle.Bundle{ + Config: config.Root{}, + } + + err := WaitForAppDeletion(ctx, b) + require.NoError(t, err) +} + +func TestWaitForAppDeletion_AppNotDeleting(t *testing.T) { + ctx := context.Background() + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Apps: map[string]*resources.App{ + "my_app": { + App: apps.App{ + Name: "my_app", + }, + }, + }, + }, + }, + } + + mwc := mocks.NewMockWorkspaceClient(t) + b.SetWorkpaceClient(mwc.WorkspaceClient) + + appApi := mwc.GetMockAppsAPI() + + appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(&apps.App{ + Name: "my_app", + ComputeStatus: &apps.ComputeStatus{ + State: apps.ComputeStateActive, + }, + }, nil).Once() + + err := WaitForAppDeletion(ctx, b) + require.NoError(t, err) +} From 5c068bde0b6caa21fc6cbc5f9ffcc3d712f59619 Mon Sep 17 00:00:00 2001 From: Varun Deep Saini Date: Sat, 29 Nov 2025 00:44:46 +0530 Subject: [PATCH 2/6] fixed deployment failing when the app in in deleting state --- bundle/deploy/prepare.go | 77 ++++++++++++++++++++ bundle/deploy/prepare_test.go | 129 ++++++++++++++++++++++++++++++++++ bundle/phases/deploy.go | 1 + 3 files changed, 207 insertions(+) create mode 100644 bundle/deploy/prepare.go create mode 100644 bundle/deploy/prepare_test.go diff --git a/bundle/deploy/prepare.go b/bundle/deploy/prepare.go new file mode 100644 index 0000000000..568b9d2945 --- /dev/null +++ b/bundle/deploy/prepare.go @@ -0,0 +1,77 @@ +package deploy + +import ( + "context" + "fmt" + "time" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/log" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/retries" + "github.com/databricks/databricks-sdk-go/service/apps" +) + +type prepareEnvironment struct{} + +func (p *prepareEnvironment) Name() string { + return "deploy:prepare-environment" +} + +// Apply runs all pre-deployment environment preparation steps. +func (p *prepareEnvironment) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { + // Check for resources in transient states and wait for them to complete. + // This prevents deployment failures due to resources still being deleted. + if err := waitForAppsDeletion(ctx, b); err != nil { + return diag.FromErr(err) + } + + return nil +} + +// PrepareEnvironment returns a mutator that prepares the environment for deployment. +// It runs checks and waits for resources in transient states before deployment proceeds. +func PrepareEnvironment() bundle.Mutator { + return &prepareEnvironment{} +} + +// waitForAppsDeletion waits for apps to be deleted if they are in DELETING state. +func waitForAppsDeletion(ctx context.Context, b *bundle.Bundle) error { + if len(b.Config.Resources.Apps) == 0 { + return nil + } + + w := b.WorkspaceClient() + + for _, app := range b.Config.Resources.Apps { + appName := app.Name + if appName == "" { + continue + } + + log.Debugf(ctx, "Checking status of app %s", appName) + + _, err := retries.Poll(ctx, 5*time.Minute, func() (*struct{}, *retries.Err) { + appStatus, err := w.Apps.GetByName(ctx, appName) + if err != nil { + if apierr.IsMissing(err) { + return nil, nil + } + return nil, retries.Halt(err) + } + + if appStatus.ComputeStatus.State == apps.ComputeStateDeleting { + log.Infof(ctx, "App %s is in DELETING state, waiting for it to be deleted...", appName) + return nil, retries.Continues("app is deleting") + } + + return nil, nil + }) + if err != nil { + return fmt.Errorf("failed to wait for app %s deletion: %w", appName, err) + } + } + + return nil +} diff --git a/bundle/deploy/prepare_test.go b/bundle/deploy/prepare_test.go new file mode 100644 index 0000000000..80b4e11fdb --- /dev/null +++ b/bundle/deploy/prepare_test.go @@ -0,0 +1,129 @@ +package deploy + +import ( + "context" + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/experimental/mocks" + "github.com/databricks/databricks-sdk-go/service/apps" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestWaitForAppsDeletion(t *testing.T) { + ctx := context.Background() + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Apps: map[string]*resources.App{ + "my_app": { + App: apps.App{ + Name: "my_app", + }, + }, + }, + }, + }, + } + + mwc := mocks.NewMockWorkspaceClient(t) + b.SetWorkpaceClient(mwc.WorkspaceClient) + + appApi := mwc.GetMockAppsAPI() + + appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(&apps.App{ + Name: "my_app", + ComputeStatus: &apps.ComputeStatus{ + State: apps.ComputeStateDeleting, + }, + }, nil).Once() + + appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(nil, &apierr.APIError{ + StatusCode: 404, + Message: "App not found", + }).Once() + + err := waitForAppsDeletion(ctx, b) + require.NoError(t, err) +} + +func TestWaitForAppsDeletion_NoApps(t *testing.T) { + ctx := context.Background() + b := &bundle.Bundle{ + Config: config.Root{}, + } + + err := waitForAppsDeletion(ctx, b) + require.NoError(t, err) +} + +func TestWaitForAppsDeletion_AppNotDeleting(t *testing.T) { + ctx := context.Background() + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Apps: map[string]*resources.App{ + "my_app": { + App: apps.App{ + Name: "my_app", + }, + }, + }, + }, + }, + } + + mwc := mocks.NewMockWorkspaceClient(t) + b.SetWorkpaceClient(mwc.WorkspaceClient) + + appApi := mwc.GetMockAppsAPI() + + appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(&apps.App{ + Name: "my_app", + ComputeStatus: &apps.ComputeStatus{ + State: apps.ComputeStateActive, + }, + }, nil).Once() + + err := waitForAppsDeletion(ctx, b) + require.NoError(t, err) +} + +func TestPrepareEnvironment(t *testing.T) { + ctx := context.Background() + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Apps: map[string]*resources.App{ + "my_app": { + App: apps.App{ + Name: "my_app", + }, + }, + }, + }, + }, + } + + mwc := mocks.NewMockWorkspaceClient(t) + b.SetWorkpaceClient(mwc.WorkspaceClient) + + appApi := mwc.GetMockAppsAPI() + + appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(&apps.App{ + Name: "my_app", + ComputeStatus: &apps.ComputeStatus{ + State: apps.ComputeStateActive, + }, + }, nil).Once() + + m := PrepareEnvironment() + require.Equal(t, "deploy:prepare-environment", m.Name()) + + diags := m.Apply(ctx, b) + require.Empty(t, diags) +} diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index b136b24303..7f6b065bd1 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -173,6 +173,7 @@ func Deploy(ctx context.Context, b *bundle.Bundle, outputHandler sync.OutputHand permissions.ApplyWorkspaceRootPermissions(), metrics.TrackUsedCompute(), deploy.ResourcePathMkdir(), + deploy.PrepareEnvironment(), ) if logdiag.HasError(ctx) { From a495c5a84c4fe785527be0194a2dd8f2e2661889 Mon Sep 17 00:00:00 2001 From: Varun Deep Saini Date: Sat, 6 Dec 2025 17:39:49 +0530 Subject: [PATCH 3/6] Updated the waiting to be done at the DAG level --- bundle/deploy/prepare.go | 77 ------------------- bundle/deploy/prepare_test.go | 129 -------------------------------- bundle/deploy/wait.go | 53 ------------- bundle/deploy/wait_test.go | 94 ----------------------- bundle/direct/dresources/app.go | 27 +++++++ bundle/phases/deploy.go | 1 - 6 files changed, 27 insertions(+), 354 deletions(-) delete mode 100644 bundle/deploy/prepare.go delete mode 100644 bundle/deploy/prepare_test.go delete mode 100644 bundle/deploy/wait.go delete mode 100644 bundle/deploy/wait_test.go diff --git a/bundle/deploy/prepare.go b/bundle/deploy/prepare.go deleted file mode 100644 index 568b9d2945..0000000000 --- a/bundle/deploy/prepare.go +++ /dev/null @@ -1,77 +0,0 @@ -package deploy - -import ( - "context" - "fmt" - "time" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/libs/diag" - "github.com/databricks/cli/libs/log" - "github.com/databricks/databricks-sdk-go/apierr" - "github.com/databricks/databricks-sdk-go/retries" - "github.com/databricks/databricks-sdk-go/service/apps" -) - -type prepareEnvironment struct{} - -func (p *prepareEnvironment) Name() string { - return "deploy:prepare-environment" -} - -// Apply runs all pre-deployment environment preparation steps. -func (p *prepareEnvironment) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { - // Check for resources in transient states and wait for them to complete. - // This prevents deployment failures due to resources still being deleted. - if err := waitForAppsDeletion(ctx, b); err != nil { - return diag.FromErr(err) - } - - return nil -} - -// PrepareEnvironment returns a mutator that prepares the environment for deployment. -// It runs checks and waits for resources in transient states before deployment proceeds. -func PrepareEnvironment() bundle.Mutator { - return &prepareEnvironment{} -} - -// waitForAppsDeletion waits for apps to be deleted if they are in DELETING state. -func waitForAppsDeletion(ctx context.Context, b *bundle.Bundle) error { - if len(b.Config.Resources.Apps) == 0 { - return nil - } - - w := b.WorkspaceClient() - - for _, app := range b.Config.Resources.Apps { - appName := app.Name - if appName == "" { - continue - } - - log.Debugf(ctx, "Checking status of app %s", appName) - - _, err := retries.Poll(ctx, 5*time.Minute, func() (*struct{}, *retries.Err) { - appStatus, err := w.Apps.GetByName(ctx, appName) - if err != nil { - if apierr.IsMissing(err) { - return nil, nil - } - return nil, retries.Halt(err) - } - - if appStatus.ComputeStatus.State == apps.ComputeStateDeleting { - log.Infof(ctx, "App %s is in DELETING state, waiting for it to be deleted...", appName) - return nil, retries.Continues("app is deleting") - } - - return nil, nil - }) - if err != nil { - return fmt.Errorf("failed to wait for app %s deletion: %w", appName, err) - } - } - - return nil -} diff --git a/bundle/deploy/prepare_test.go b/bundle/deploy/prepare_test.go deleted file mode 100644 index 80b4e11fdb..0000000000 --- a/bundle/deploy/prepare_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package deploy - -import ( - "context" - "testing" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/bundle/config/resources" - "github.com/databricks/databricks-sdk-go/apierr" - "github.com/databricks/databricks-sdk-go/experimental/mocks" - "github.com/databricks/databricks-sdk-go/service/apps" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestWaitForAppsDeletion(t *testing.T) { - ctx := context.Background() - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Apps: map[string]*resources.App{ - "my_app": { - App: apps.App{ - Name: "my_app", - }, - }, - }, - }, - }, - } - - mwc := mocks.NewMockWorkspaceClient(t) - b.SetWorkpaceClient(mwc.WorkspaceClient) - - appApi := mwc.GetMockAppsAPI() - - appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(&apps.App{ - Name: "my_app", - ComputeStatus: &apps.ComputeStatus{ - State: apps.ComputeStateDeleting, - }, - }, nil).Once() - - appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(nil, &apierr.APIError{ - StatusCode: 404, - Message: "App not found", - }).Once() - - err := waitForAppsDeletion(ctx, b) - require.NoError(t, err) -} - -func TestWaitForAppsDeletion_NoApps(t *testing.T) { - ctx := context.Background() - b := &bundle.Bundle{ - Config: config.Root{}, - } - - err := waitForAppsDeletion(ctx, b) - require.NoError(t, err) -} - -func TestWaitForAppsDeletion_AppNotDeleting(t *testing.T) { - ctx := context.Background() - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Apps: map[string]*resources.App{ - "my_app": { - App: apps.App{ - Name: "my_app", - }, - }, - }, - }, - }, - } - - mwc := mocks.NewMockWorkspaceClient(t) - b.SetWorkpaceClient(mwc.WorkspaceClient) - - appApi := mwc.GetMockAppsAPI() - - appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(&apps.App{ - Name: "my_app", - ComputeStatus: &apps.ComputeStatus{ - State: apps.ComputeStateActive, - }, - }, nil).Once() - - err := waitForAppsDeletion(ctx, b) - require.NoError(t, err) -} - -func TestPrepareEnvironment(t *testing.T) { - ctx := context.Background() - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Apps: map[string]*resources.App{ - "my_app": { - App: apps.App{ - Name: "my_app", - }, - }, - }, - }, - }, - } - - mwc := mocks.NewMockWorkspaceClient(t) - b.SetWorkpaceClient(mwc.WorkspaceClient) - - appApi := mwc.GetMockAppsAPI() - - appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(&apps.App{ - Name: "my_app", - ComputeStatus: &apps.ComputeStatus{ - State: apps.ComputeStateActive, - }, - }, nil).Once() - - m := PrepareEnvironment() - require.Equal(t, "deploy:prepare-environment", m.Name()) - - diags := m.Apply(ctx, b) - require.Empty(t, diags) -} diff --git a/bundle/deploy/wait.go b/bundle/deploy/wait.go deleted file mode 100644 index 7244a39603..0000000000 --- a/bundle/deploy/wait.go +++ /dev/null @@ -1,53 +0,0 @@ -package deploy - -import ( - "context" - "fmt" - "time" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/libs/log" - "github.com/databricks/databricks-sdk-go/apierr" - "github.com/databricks/databricks-sdk-go/retries" - "github.com/databricks/databricks-sdk-go/service/apps" -) - -// WaitForAppDeletion waits for apps to be deleted if they are in DELETING state. -func WaitForAppDeletion(ctx context.Context, b *bundle.Bundle) error { - if len(b.Config.Resources.Apps) == 0 { - return nil - } - - w := b.WorkspaceClient() - - for _, app := range b.Config.Resources.Apps { - appName := app.Name - if appName == "" { - continue - } - - log.Debugf(ctx, "Checking status of app %s", appName) - - _, err := retries.Poll(ctx, 10*time.Minute, func() (*struct{}, *retries.Err) { - appStatus, err := w.Apps.GetByName(ctx, appName) - if err != nil { - if apierr.IsMissing(err) { - return nil, nil - } - return nil, retries.Halt(err) - } - - if appStatus.ComputeStatus.State == apps.ComputeStateDeleting { - log.Infof(ctx, "App %s is in DELETING state, waiting for it to be deleted...", appName) - return nil, retries.Continues("app is deleting") - } - - return nil, nil - }) - if err != nil { - return fmt.Errorf("failed to wait for app %s deletion: %w", appName, err) - } - } - - return nil -} diff --git a/bundle/deploy/wait_test.go b/bundle/deploy/wait_test.go deleted file mode 100644 index eb47912df2..0000000000 --- a/bundle/deploy/wait_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package deploy - -import ( - "context" - "testing" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/bundle/config/resources" - "github.com/databricks/databricks-sdk-go/apierr" - "github.com/databricks/databricks-sdk-go/experimental/mocks" - "github.com/databricks/databricks-sdk-go/service/apps" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestWaitForAppDeletion(t *testing.T) { - ctx := context.Background() - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Apps: map[string]*resources.App{ - "my_app": { - App: apps.App{ - Name: "my_app", - }, - }, - }, - }, - }, - } - - mwc := mocks.NewMockWorkspaceClient(t) - b.SetWorkpaceClient(mwc.WorkspaceClient) - - appApi := mwc.GetMockAppsAPI() - - appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(&apps.App{ - Name: "my_app", - ComputeStatus: &apps.ComputeStatus{ - State: apps.ComputeStateDeleting, - }, - }, nil).Once() - - appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(nil, &apierr.APIError{ - StatusCode: 404, - Message: "App not found", - }).Once() - - err := WaitForAppDeletion(ctx, b) - require.NoError(t, err) -} - -func TestWaitForAppDeletion_NoApps(t *testing.T) { - ctx := context.Background() - b := &bundle.Bundle{ - Config: config.Root{}, - } - - err := WaitForAppDeletion(ctx, b) - require.NoError(t, err) -} - -func TestWaitForAppDeletion_AppNotDeleting(t *testing.T) { - ctx := context.Background() - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Apps: map[string]*resources.App{ - "my_app": { - App: apps.App{ - Name: "my_app", - }, - }, - }, - }, - }, - } - - mwc := mocks.NewMockWorkspaceClient(t) - b.SetWorkpaceClient(mwc.WorkspaceClient) - - appApi := mwc.GetMockAppsAPI() - - appApi.EXPECT().GetByName(mock.Anything, "my_app").Return(&apps.App{ - Name: "my_app", - ComputeStatus: &apps.ComputeStatus{ - State: apps.ComputeStateActive, - }, - }, nil).Once() - - err := WaitForAppDeletion(ctx, b) - require.NoError(t, err) -} diff --git a/bundle/direct/dresources/app.go b/bundle/direct/dresources/app.go index ed0c88813d..4085c15052 100644 --- a/bundle/direct/dresources/app.go +++ b/bundle/direct/dresources/app.go @@ -8,6 +8,7 @@ import ( "github.com/databricks/cli/bundle/deployplan" "github.com/databricks/cli/libs/log" "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/databricks-sdk-go/retries" "github.com/databricks/databricks-sdk-go/service/apps" ) @@ -29,6 +30,10 @@ func (r *ResourceApp) DoRead(ctx context.Context, id string) (*apps.App, error) } func (r *ResourceApp) DoCreate(ctx context.Context, config *apps.App) (string, *apps.App, error) { + if err := r.waitForDeletion(ctx, config.Name); err != nil { + return "", nil, err + } + request := apps.CreateAppRequest{ App: *config, NoCompute: true, @@ -74,6 +79,28 @@ func (r *ResourceApp) WaitAfterCreate(ctx context.Context, config *apps.App) (*a return r.waitForApp(ctx, r.client, config.Name) } +// waitForDeletion waits for an app to be deleted if it exists and is in DELETING state. +func (r *ResourceApp) waitForDeletion(ctx context.Context, name string) error { + retrier := retries.New[struct{}](retries.WithTimeout(-1), retries.WithRetryFunc(shouldRetry)) + _, err := retrier.Run(ctx, func(ctx context.Context) (*struct{}, error) { + app, err := r.client.Apps.GetByName(ctx, name) + if err != nil { + if apierr.IsMissing(err) { + return nil, nil + } + return nil, retries.Halt(err) + } + + if app.ComputeStatus.State == apps.ComputeStateDeleting { + log.Infof(ctx, "App %s is in DELETING state, waiting for it to be deleted...", name) + return nil, retries.Continues("app is deleting") + } + + return nil, nil + }) + return err +} + // waitForApp waits for the app to reach the target state. The target state is either ACTIVE or STOPPED. // Apps with no_compute set to true will reach the STOPPED state, otherwise they will reach the ACTIVE state. // We can't use the default waiter from SDK because it only waits on ACTIVE state but we need also STOPPED state. diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index 7f6b065bd1..b136b24303 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -173,7 +173,6 @@ func Deploy(ctx context.Context, b *bundle.Bundle, outputHandler sync.OutputHand permissions.ApplyWorkspaceRootPermissions(), metrics.TrackUsedCompute(), deploy.ResourcePathMkdir(), - deploy.PrepareEnvironment(), ) if logdiag.HasError(ctx) { From 9a21ffa52bc36e3c3f674571371f180fe94e71dc Mon Sep 17 00:00:00 2001 From: Varun Deep Saini Date: Sat, 6 Dec 2025 18:23:43 +0530 Subject: [PATCH 4/6] nil check --- bundle/direct/dresources/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle/direct/dresources/app.go b/bundle/direct/dresources/app.go index 4085c15052..18f06eaeff 100644 --- a/bundle/direct/dresources/app.go +++ b/bundle/direct/dresources/app.go @@ -91,7 +91,7 @@ func (r *ResourceApp) waitForDeletion(ctx context.Context, name string) error { return nil, retries.Halt(err) } - if app.ComputeStatus.State == apps.ComputeStateDeleting { + if app.ComputeStatus != nil && app.ComputeStatus.State == apps.ComputeStateDeleting { log.Infof(ctx, "App %s is in DELETING state, waiting for it to be deleted...", name) return nil, retries.Continues("app is deleting") } From 3990bd0d0ab91671ccd3d746e8797a4071498124 Mon Sep 17 00:00:00 2001 From: Varun Deep Saini Date: Tue, 9 Dec 2025 17:25:33 +0530 Subject: [PATCH 5/6] moved the waiting for in the delete function --- bundle/direct/dresources/app.go | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/bundle/direct/dresources/app.go b/bundle/direct/dresources/app.go index 18f06eaeff..370d56a8eb 100644 --- a/bundle/direct/dresources/app.go +++ b/bundle/direct/dresources/app.go @@ -3,6 +3,7 @@ package dresources import ( "context" "fmt" + "time" "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/cli/bundle/deployplan" @@ -30,10 +31,6 @@ func (r *ResourceApp) DoRead(ctx context.Context, id string) (*apps.App, error) } func (r *ResourceApp) DoCreate(ctx context.Context, config *apps.App) (string, *apps.App, error) { - if err := r.waitForDeletion(ctx, config.Name); err != nil { - return "", nil, err - } - request := apps.CreateAppRequest{ App: *config, NoCompute: true, @@ -66,7 +63,10 @@ func (r *ResourceApp) DoUpdate(ctx context.Context, id string, config *apps.App, func (r *ResourceApp) DoDelete(ctx context.Context, id string) error { _, err := r.client.Apps.DeleteByName(ctx, id) - return err + if err != nil { + return err + } + return r.waitForDeletion(ctx, id) } func (*ResourceApp) FieldTriggers(_ bool) map[string]deployplan.ActionType { @@ -79,9 +79,9 @@ func (r *ResourceApp) WaitAfterCreate(ctx context.Context, config *apps.App) (*a return r.waitForApp(ctx, r.client, config.Name) } -// waitForDeletion waits for an app to be deleted if it exists and is in DELETING state. +// waitForDeletion waits for an app to be fully deleted. func (r *ResourceApp) waitForDeletion(ctx context.Context, name string) error { - retrier := retries.New[struct{}](retries.WithTimeout(-1), retries.WithRetryFunc(shouldRetry)) + retrier := retries.New[struct{}](retries.WithTimeout(10*time.Minute), retries.WithRetryFunc(shouldRetry)) _, err := retrier.Run(ctx, func(ctx context.Context) (*struct{}, error) { app, err := r.client.Apps.GetByName(ctx, name) if err != nil { @@ -91,12 +91,20 @@ func (r *ResourceApp) waitForDeletion(ctx context.Context, name string) error { return nil, retries.Halt(err) } - if app.ComputeStatus != nil && app.ComputeStatus.State == apps.ComputeStateDeleting { + if app.ComputeStatus == nil { + return nil, retries.Continues("waiting for compute status") + } + + switch app.ComputeStatus.State { + case apps.ComputeStateDeleting: log.Infof(ctx, "App %s is in DELETING state, waiting for it to be deleted...", name) return nil, retries.Continues("app is deleting") + case apps.ComputeStateActive, apps.ComputeStateStopped, apps.ComputeStateError: + err := fmt.Errorf("app %s was not deleted, current state: %s", name, app.ComputeStatus.State) + return nil, retries.Halt(err) + default: + return nil, retries.Continues(fmt.Sprintf("app is in %s state", app.ComputeStatus.State)) } - - return nil, nil }) return err } From 0f31f501eab03fab98a51c1b1838f9146f7af6bf Mon Sep 17 00:00:00 2001 From: Varun Deep Saini Date: Tue, 9 Dec 2025 17:32:28 +0530 Subject: [PATCH 6/6] formatting --- bundle/direct/dresources/app.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/bundle/direct/dresources/app.go b/bundle/direct/dresources/app.go index 370d56a8eb..e2ffdb9496 100644 --- a/bundle/direct/dresources/app.go +++ b/bundle/direct/dresources/app.go @@ -79,7 +79,6 @@ func (r *ResourceApp) WaitAfterCreate(ctx context.Context, config *apps.App) (*a return r.waitForApp(ctx, r.client, config.Name) } -// waitForDeletion waits for an app to be fully deleted. func (r *ResourceApp) waitForDeletion(ctx context.Context, name string) error { retrier := retries.New[struct{}](retries.WithTimeout(10*time.Minute), retries.WithRetryFunc(shouldRetry)) _, err := retrier.Run(ctx, func(ctx context.Context) (*struct{}, error) { @@ -97,7 +96,6 @@ func (r *ResourceApp) waitForDeletion(ctx context.Context, name string) error { switch app.ComputeStatus.State { case apps.ComputeStateDeleting: - log.Infof(ctx, "App %s is in DELETING state, waiting for it to be deleted...", name) return nil, retries.Continues("app is deleting") case apps.ComputeStateActive, apps.ComputeStateStopped, apps.ComputeStateError: err := fmt.Errorf("app %s was not deleted, current state: %s", name, app.ComputeStatus.State)