Repository: go-to-k/delstack (go)

Issue:
[Feature] throws error if a stack with XxxInProgress is specified
Originally not displayed in interactive mode, it still does not generate an error when the stack name is directly specified.

Pull request:
feat(operation): throws error if a stack with XxxInProgress is specified
Closes #297

Patch:
diff --git a/internal/operation/cloudformation_stack.go b/internal/operation/cloudformation_stack.go
index 4d71d34..099e3bf 100644
--- a/internal/operation/cloudformation_stack.go
+++ b/internal/operation/cloudformation_stack.go
@@ -19,6 +19,21 @@ var _ IOperator = (*CloudFormationStackOperator)(nil)
 
 const StackNameRule = `^arn:aws:cloudformation:[^:]*:[0-9]*:stack/([^/]*)/.*$`
 
+// Except xxInProgress
+// StackStatusDeleteComplete is not included because DescribeStacks does not return a DELETE_COMPLETE stack.
+var StackStatusExceptionsForDescribeStacks = []types.StackStatus{
+	types.StackStatusCreateInProgress,
+	types.StackStatusRollbackInProgress,
+	types.StackStatusDeleteInProgress,
+	types.StackStatusUpdateInProgress,
+	types.StackStatusUpdateCompleteCleanupInProgress,
+	types.StackStatusUpdateRollbackInProgress,
+	types.StackStatusUpdateRollbackCompleteCleanupInProgress,
+	types.StackStatusReviewInProgress,
+	types.StackStatusImportInProgress,
+	types.StackStatusImportRollbackInProgress,
+}
+
 var StackNameRuleRegExp = regexp.MustCompile(StackNameRule)
 
 type CloudFormationStackOperator struct {
@@ -117,6 +132,9 @@ func (o *CloudFormationStackOperator) deleteStackNormally(ctx context.Context, s
 	if stacksBeforeDelete[0].EnableTerminationProtection != nil && *stacksBeforeDelete[0].EnableTerminationProtection {
 		return false, fmt.Errorf("TerminationProtectionIsEnabled: %v", *stackName)
 	}
+	if o.isExceptedByStackStatus(stacksBeforeDelete[0].StackStatus) {
+		return false, fmt.Errorf("OperationInProgressError: Stacks with XxxInProgress cannot be deleted, but %v: %v", stacksBeforeDelete[0].StackStatus, *stackName)
+	}
 
 	if err := o.client.DeleteStack(ctx, stackName, []string{}); err != nil {
 		return false, err
@@ -140,22 +158,6 @@ func (o *CloudFormationStackOperator) deleteStackNormally(ctx context.Context, s
 func (o *CloudFormationStackOperator) ListStacksFilteredByKeyword(ctx context.Context, keyword *string) ([]string, error) {
 	filteredStacks := []string{}
 
-	// Except StackStatusDeleteComplete and xxInProgress
-	// In fact, DescribeStacks does not return a DELETE_COMPLETE stack, but it is included just in case.
-	stackStatusExceptionFilter := []types.StackStatus{
-		types.StackStatusCreateInProgress,
-		types.StackStatusRollbackInProgress,
-		types.StackStatusDeleteInProgress,
-		types.StackStatusUpdateInProgress,
-		types.StackStatusUpdateCompleteCleanupInProgress,
-		types.StackStatusUpdateRollbackInProgress,
-		types.StackStatusUpdateRollbackCompleteCleanupInProgress,
-		types.StackStatusReviewInProgress,
-		types.StackStatusImportInProgress,
-		types.StackStatusImportRollbackInProgress,
-		types.StackStatusDeleteComplete,
-	}
-
 	// Use DescribeStacks instead of ListStacks to take EnableTerminationProtection
 	stacks, err := o.client.DescribeStacks(ctx, nil)
 	if err != nil {
@@ -176,14 +178,7 @@ func (o *CloudFormationStackOperator) ListStacksFilteredByKeyword(ctx context.Co
 		}
 
 		// except the stacks that are in the exception list
-		shouldExcept := false
-		for _, status := range stackStatusExceptionFilter {
-			if stack.StackStatus == status {
-				shouldExcept = true
-				break
-			}
-		}
-		if shouldExcept {
+		if o.isExceptedByStackStatus(stack.StackStatus) {
 			continue
 		}
 
@@ -196,3 +191,12 @@ func (o *CloudFormationStackOperator) ListStacksFilteredByKeyword(ctx context.Co
 
 	return filteredStacks, nil
 }
+
+func (o *CloudFormationStackOperator) isExceptedByStackStatus(stackStatus types.StackStatus) bool {
+	for _, status := range StackStatusExceptionsForDescribeStacks {
+		if stackStatus == status {
+			return true
+		}
+	}
+	return false
+}


Test patch:
diff --git a/internal/operation/cloudformation_stack_test.go b/internal/operation/cloudformation_stack_test.go
index e3bffb1..97a60ff 100644
--- a/internal/operation/cloudformation_stack_test.go
+++ b/internal/operation/cloudformation_stack_test.go
@@ -1384,6 +1384,31 @@ func TestCloudFormationStackOperator_deleteStackNormally(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name: "delete stack failure for root stack with operation in progress",
+			args: args{
+				ctx:         context.Background(),
+				stackName:   aws.String("test"),
+				isRootStack: false,
+			},
+			prepareMockCloudFormationFn: func(m *client.MockICloudFormation) {
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("test")).Return(
+					[]types.Stack{
+						{
+							StackName:                   aws.String("test"),
+							StackStatus:                 "DELETE_IN_PROGRESS",
+							EnableTerminationProtection: aws.Bool(false),
+						},
+					},
+					nil,
+				)
+			},
+			want: want{
+				got: false,
+				err: fmt.Errorf("OperationInProgressError: Stacks with XxxInProgress cannot be deleted, but DELETE_IN_PROGRESS: test"),
+			},
+			wantErr: true,
+		},
 	}
 
 	for _, tt := range cases {
@@ -1706,34 +1731,6 @@ func TestCloudFormationStackOperator_ListStacksFilteredByKeyword(t *testing.T) {
 			},
 			wantErr: false,
 		},
-		// In fact, DescribeStacks does not return a DELETE_COMPLETE stack, but it is included just in case.
-		{
-			name: "list stacks filtered by keyword with DeleteComplete stacks successfully",
-			args: args{
-				ctx:     ctx,
-				keyword: "TestStack",
-			},
-			prepareMockCloudFormationFn: func(m *client.MockICloudFormation) {
-				m.EXPECT().DescribeStacks(gomock.Any(), nil).Return(
-					[]types.Stack{
-						{
-							StackName:   aws.String("TestStack1"),
-							StackStatus: types.StackStatusDeleteComplete,
-						},
-						{
-							StackName:   aws.String("TestStack2"),
-							StackStatus: types.StackStatusDeleteComplete,
-						},
-					},
-					nil,
-				)
-			},
-			want: want{
-				filteredStacks: []string{},
-				err:            nil,
-			},
-			wantErr: false,
-		},
 		{
 			name: "list stacks filtered by keyword with EnableTerminationProtection stacks successfully",
 			args: args{