From d9261d31ddb3ed0c7dc57937664bab90c6fdd9ac Mon Sep 17 00:00:00 2001 From: shivamj-del Date: Thu, 29 Oct 2026 06:01:42 +0530 Subject: [PATCH] fix: block delete while instance is still provisioning --- pkg/cmd/delete/delete.go | 24 ++++++++++++++++++++++-- pkg/environment/status.go | 13 +++++++++++++ pkg/environment/status_test.go | 24 ++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/delete/delete.go b/pkg/cmd/delete/delete.go index 4e56d8fcd..d99c7df1f 100644 --- a/pkg/cmd/delete/delete.go +++ b/pkg/cmd/delete/delete.go @@ -1,15 +1,18 @@ package delete import ( + "context" _ "embed" "fmt" "os" "strings" + devplanev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1" "github.com/brevdev/brev-cli/pkg/auth" "github.com/brevdev/brev-cli/pkg/cmd/completions" "github.com/brevdev/brev-cli/pkg/cmd/util" "github.com/brevdev/brev-cli/pkg/entity" + "github.com/brevdev/brev-cli/pkg/environment" breverrors "github.com/brevdev/brev-cli/pkg/errors" "github.com/brevdev/brev-cli/pkg/terminal" "github.com/hashicorp/go-multierror" @@ -27,6 +30,7 @@ type DeleteStore interface { completions.CompletionStore DeleteWorkspace(workspaceID string) (*entity.Workspace, error) GetWorkspaceByNameOrID(orgID string, nameOrID string) ([]entity.Workspace, error) + GetEnvironment(ctx context.Context, environmentID string) (*devplanev1.Environment, error) } func NewCmdDelete(t *terminal.Terminal, loginDeleteStore DeleteStore, noLoginDeleteStore DeleteStore) *cobra.Command { @@ -47,7 +51,7 @@ func NewCmdDelete(t *terminal.Terminal, loginDeleteStore DeleteStore, noLoginDel var allError error var deletedNames []string for _, workspace := range names { - err := deleteWorkspace(workspace, t, loginDeleteStore, piped) + err := deleteWorkspace(cmd.Context(), workspace, t, loginDeleteStore, piped) if err != nil { allError = multierror.Append(allError, err) } else { @@ -70,7 +74,7 @@ func NewCmdDelete(t *terminal.Terminal, loginDeleteStore DeleteStore, noLoginDel return cmd } -func deleteWorkspace(workspaceName string, t *terminal.Terminal, deleteStore DeleteStore, piped bool) error { +func deleteWorkspace(ctx context.Context, workspaceName string, t *terminal.Terminal, deleteStore DeleteStore, piped bool) error { workspace, err := util.GetUserWorkspaceByNameOrIDErr(deleteStore, workspaceName) if err != nil { err1 := handleAdminUser(err, deleteStore, piped) @@ -81,6 +85,15 @@ func deleteWorkspace(workspaceName string, t *terminal.Terminal, deleteStore Del var workspaceID string if workspace != nil { + env, err := deleteStore.GetEnvironment(ctx, workspace.ID) + if err != nil { + return breverrors.WrapAndTrace(err) + } + if phase := setupPhase(workspace, env); phase != "" { + return breverrors.NewValidationError(fmt.Sprintf( + "instance %q is still %s and can't be deleted yet. Run 'brev ls' and try again once it's running.", + workspace.Name, phase)) + } workspaceID = workspace.ID } else { workspaceID = workspaceName @@ -98,6 +111,13 @@ func deleteWorkspace(workspaceName string, t *terminal.Terminal, deleteStore Del return nil } +func setupPhase(workspace *entity.Workspace, env *devplanev1.Environment) string { + if env.GetStatus() == devplanev1.EnvironmentStatus_ENVIRONMENT_STATUS_UNSPECIFIED && workspace.Status == entity.Deploying { + return "deploying" + } + return environment.SetupPhase(env) +} + func handleAdminUser(err error, deleteStore DeleteStore, piped bool) error { if strings.Contains(err.Error(), "not found") { if auth.IsAPIKeyAuthStore(deleteStore) { diff --git a/pkg/environment/status.go b/pkg/environment/status.go index efffcdcf5..10061a7e3 100644 --- a/pkg/environment/status.go +++ b/pkg/environment/status.go @@ -74,3 +74,16 @@ func IsFailed(env *devplanev1.Environment) bool { } return false } + +// SetupPhase returns the setup phase the environment is in, or "" once setup is over. +func SetupPhase(env *devplanev1.Environment) string { + switch env.GetStatus() { + case devplanev1.EnvironmentStatus_ENVIRONMENT_STATUS_PENDING: + return "pending" + case devplanev1.EnvironmentStatus_ENVIRONMENT_STATUS_PREPARING: + return "preparing" + case devplanev1.EnvironmentStatus_ENVIRONMENT_STATUS_PROVISIONING: + return "provisioning" + } + return "" +} diff --git a/pkg/environment/status_test.go b/pkg/environment/status_test.go index 031a83c53..81806310c 100644 --- a/pkg/environment/status_test.go +++ b/pkg/environment/status_test.go @@ -47,6 +47,30 @@ func TestStatusDisplayMapsEnvironmentStatuses(t *testing.T) { } } +func TestSetupPhase(t *testing.T) { + cases := []struct { + name string + status devplanev1.EnvironmentStatus + want string + }{ + {"pending", devplanev1.EnvironmentStatus_ENVIRONMENT_STATUS_PENDING, "pending"}, + {"preparing", devplanev1.EnvironmentStatus_ENVIRONMENT_STATUS_PREPARING, "preparing"}, + {"provisioning", devplanev1.EnvironmentStatus_ENVIRONMENT_STATUS_PROVISIONING, "provisioning"}, + {"preparing_failed", devplanev1.EnvironmentStatus_ENVIRONMENT_STATUS_PREPARING_FAILED, ""}, + {"provisioning_failed", devplanev1.EnvironmentStatus_ENVIRONMENT_STATUS_PROVISIONING_FAILED, ""}, + {"building", devplanev1.EnvironmentStatus_ENVIRONMENT_STATUS_BUILDING, ""}, + {"starting", devplanev1.EnvironmentStatus_ENVIRONMENT_STATUS_STARTING, ""}, + {"running", devplanev1.EnvironmentStatus_ENVIRONMENT_STATUS_RUNNING, ""}, + {"unspecified", devplanev1.EnvironmentStatus_ENVIRONMENT_STATUS_UNSPECIFIED, ""}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, SetupPhase(&devplanev1.Environment{Status: tc.status})) + }) + } +} + // TestFailureMessage covers the provider-message preference and fallbacks. func TestFailureMessage(t *testing.T) { cases := []struct {