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

Issue:
[Feature] Check all stacks for TerminationProtection before starting deletion when multiple stacks
**Is your feature request related to a problem? Please describe.**
When deleting multiple stacks, if the stack executed in the middle has TerminationProtection, it will terminate with an error on that stack after the previous stack is deleted. Better to know it first and not start deleting all the stacks.

**Describe the solution you'd like**
Check all stacks for TerminationProtection before starting deletion when multiple stacks.

**Additional context**
> Add any other context or screenshots about the feature request here.


Pull request:
feat(operation): check all stacks for TerminationProtection before starting deletion when multiple stacks
Closes #344 .


Patch:
diff --git a/internal/operation/cloudformation_stack.go b/internal/operation/cloudformation_stack.go
index 5e14e2f..e80222f 100644
--- a/internal/operation/cloudformation_stack.go
+++ b/internal/operation/cloudformation_stack.go
@@ -123,7 +123,7 @@ func (o *CloudFormationStackOperator) deleteStackNormally(ctx context.Context, s
 		return false, err
 	}
 	if len(stacksBeforeDelete) == 0 && isRootStack {
-		errMsg := fmt.Sprintf("%s stack not found.", *stackName)
+		errMsg := fmt.Sprintf("%s not found", *stackName)
 		return false, fmt.Errorf("NotExistsError: %v", errMsg)
 	}
 	if len(stacksBeforeDelete) == 0 {
@@ -131,7 +131,7 @@ func (o *CloudFormationStackOperator) deleteStackNormally(ctx context.Context, s
 	}
 
 	if stacksBeforeDelete[0].EnableTerminationProtection != nil && *stacksBeforeDelete[0].EnableTerminationProtection {
-		return false, fmt.Errorf("TerminationProtectionIsEnabled: %v", *stackName)
+		return false, fmt.Errorf("TerminationProtectionError: %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)
@@ -160,6 +160,7 @@ func (o *CloudFormationStackOperator) GetSortedStackNames(ctx context.Context, s
 	sortedStackNames := []string{}
 	gotStacks := []types.Stack{}
 	notFoundStackNames := []string{}
+	terminationProtectionStackNames := []string{}
 
 	type stackNameInProgress struct {
 		stackName   string
@@ -177,6 +178,14 @@ func (o *CloudFormationStackOperator) GetSortedStackNames(ctx context.Context, s
 			notFoundStackNames = append(notFoundStackNames, stackName)
 			continue
 		}
+
+		// except the stacks with EnableTerminationProtection
+		if stack[0].EnableTerminationProtection != nil && *stack[0].EnableTerminationProtection {
+			terminationProtectionStackNames = append(terminationProtectionStackNames, stackName)
+			continue
+		}
+
+		// except the stacks that are in the exception list
 		if o.isExceptedByStackStatus(stack[0].StackStatus) {
 			stackNamesInProgress = append(stackNamesInProgress, stackNameInProgress{
 				stackName:   stackName,
@@ -184,13 +193,17 @@ func (o *CloudFormationStackOperator) GetSortedStackNames(ctx context.Context, s
 			})
 			continue
 		}
+
 		gotStacks = append(gotStacks, stack[0]) // DescribeStacks returns a stack with a single element
 	}
 
 	if len(notFoundStackNames) > 0 {
-		errMsg := fmt.Sprintf("%s stack not found.", strings.Join(notFoundStackNames, ", "))
+		errMsg := fmt.Sprintf("%s not found", strings.Join(notFoundStackNames, ", "))
 		return sortedStackNames, fmt.Errorf("NotExistsError: %v", errMsg)
 	}
+	if len(terminationProtectionStackNames) > 0 {
+		return sortedStackNames, fmt.Errorf("TerminationProtectionError: %v", strings.Join(terminationProtectionStackNames, ", "))
+	}
 	if len(stackNamesInProgress) > 0 {
 		var stackNamesWithStatus []string
 		for _, stack := range stackNamesInProgress {
@@ -245,7 +258,7 @@ func (o *CloudFormationStackOperator) ListStacksFilteredByKeyword(ctx context.Co
 	}
 
 	if len(filteredStacks) == 0 {
-		errMsg := fmt.Sprintf("No stacks matching the keyword (%s).", *keyword)
+		errMsg := fmt.Sprintf("No stacks matching the keyword (%s)", *keyword)
 		return filteredStacks, fmt.Errorf("NotExistsError: %v", errMsg)
 	}
 


Test patch:
diff --git a/internal/operation/cloudformation_stack_test.go b/internal/operation/cloudformation_stack_test.go
index 22afd09..d1e7b8e 100644
--- a/internal/operation/cloudformation_stack_test.go
+++ b/internal/operation/cloudformation_stack_test.go
@@ -115,7 +115,7 @@ func TestCloudFormationStackOperator_DeleteCloudFormationStack(t *testing.T) {
 				)
 			},
 			prepareMockOperatorManagerFn: func(m *MockIOperatorManager) {},
-			want:                         fmt.Errorf("TerminationProtectionIsEnabled: test"),
+			want:                         fmt.Errorf("TerminationProtectionError: test"),
 			wantErr:                      true,
 		},
 		{
@@ -138,7 +138,7 @@ func TestCloudFormationStackOperator_DeleteCloudFormationStack(t *testing.T) {
 				)
 			},
 			prepareMockOperatorManagerFn: func(m *MockIOperatorManager) {},
-			want:                         fmt.Errorf("TerminationProtectionIsEnabled: test"),
+			want:                         fmt.Errorf("TerminationProtectionError: test"),
 			wantErr:                      true,
 		},
 		{
@@ -289,7 +289,7 @@ func TestCloudFormationStackOperator_DeleteCloudFormationStack(t *testing.T) {
 				)
 			},
 			prepareMockOperatorManagerFn: func(m *MockIOperatorManager) {},
-			want:                         fmt.Errorf("NotExistsError: test stack not found."),
+			want:                         fmt.Errorf("NotExistsError: test not found"),
 			wantErr:                      true,
 		},
 		{
@@ -1044,7 +1044,7 @@ func TestCloudFormationStackOperator_deleteStackNormally(t *testing.T) {
 			},
 			want: want{
 				got: false,
-				err: fmt.Errorf("TerminationProtectionIsEnabled: test"),
+				err: fmt.Errorf("TerminationProtectionError: test"),
 			},
 			wantErr: true,
 		},
@@ -1069,7 +1069,7 @@ func TestCloudFormationStackOperator_deleteStackNormally(t *testing.T) {
 			},
 			want: want{
 				got: false,
-				err: fmt.Errorf("TerminationProtectionIsEnabled: test"),
+				err: fmt.Errorf("TerminationProtectionError: test"),
 			},
 			wantErr: true,
 		},
@@ -1234,7 +1234,7 @@ func TestCloudFormationStackOperator_deleteStackNormally(t *testing.T) {
 			},
 			want: want{
 				got: false,
-				err: fmt.Errorf("NotExistsError: test stack not found."),
+				err: fmt.Errorf("NotExistsError: test not found"),
 			},
 			wantErr: true,
 		},
@@ -1657,29 +1657,270 @@ func TestCloudFormationStackOperator_GetSortedStackNames(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name: "sort stacks failure for non existent stacks",
+			args: args{
+				ctx:        ctx,
+				stackNames: []string{"Stack1", "Stack2", "Stack3", "Stack4", "Stack5"},
+			},
+			prepareMockCloudFormationFn: func(m *client.MockICloudFormation) {
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack1")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack1"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now().Add(-40 * time.Minute)),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack2")).Return(
+					[]types.Stack{},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack3")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack3"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now().Add(-20 * time.Minute)),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack4")).Return(
+					[]types.Stack{},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack5")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack5"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now()),
+						},
+					},
+					nil,
+				)
+			},
+			want: want{
+				sortedStackNames: []string{},
+				err:              fmt.Errorf("NotExistsError: Stack2, Stack4 not found"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "sort stacks failure for EnableTerminationProtection stacks",
+			args: args{
+				ctx:        ctx,
+				stackNames: []string{"Stack1", "Stack2", "Stack3", "Stack4", "Stack5"},
+			},
+			prepareMockCloudFormationFn: func(m *client.MockICloudFormation) {
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack1")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack1"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now().Add(-40 * time.Minute)),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack2")).Return(
+					[]types.Stack{
+						{
+							StackName:                   aws.String("Stack2"),
+							StackStatus:                 types.StackStatusCreateComplete,
+							CreationTime:                aws.Time(time.Now().Add(-30 * time.Minute)),
+							EnableTerminationProtection: aws.Bool(true),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack3")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack3"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now().Add(-20 * time.Minute)),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack4")).Return(
+					[]types.Stack{
+						{
+							StackName:                   aws.String("Stack4"),
+							StackStatus:                 types.StackStatusCreateComplete,
+							CreationTime:                aws.Time(time.Now().Add(-10 * time.Minute)),
+							EnableTerminationProtection: aws.Bool(true),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack5")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack5"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now()),
+						},
+					},
+					nil,
+				)
+			},
+			want: want{
+				sortedStackNames: []string{},
+				err:              fmt.Errorf("TerminationProtectionError: Stack2, Stack4"),
+			},
+			wantErr: true,
+		},
 		{
 			name: "sort stacks failure for stacks in progress",
 			args: args{
 				ctx:        ctx,
-				stackNames: []string{"Stack1", "Stack2", "Stack3"},
+				stackNames: []string{"Stack1", "Stack2", "Stack3", "Stack4", "Stack5"},
 			},
 			prepareMockCloudFormationFn: func(m *client.MockICloudFormation) {
 				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack1")).Return(
 					[]types.Stack{
 						{
 							StackName:    aws.String("Stack1"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now().Add(-40 * time.Minute)),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack2")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack2"),
 							StackStatus:  types.StackStatusUpdateInProgress,
+							CreationTime: aws.Time(time.Now().Add(-30 * time.Minute)),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack3")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack3"),
+							StackStatus:  types.StackStatusCreateComplete,
 							CreationTime: aws.Time(time.Now().Add(-20 * time.Minute)),
 						},
 					},
 					nil,
 				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack4")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack4"),
+							StackStatus:  types.StackStatusRollbackInProgress,
+							CreationTime: aws.Time(time.Now().Add(-10 * time.Minute)),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack5")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack5"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now()),
+						},
+					},
+					nil,
+				)
+			},
+			want: want{
+				sortedStackNames: []string{},
+				err:              fmt.Errorf("OperationInProgressError: Stacks with XxxInProgress cannot be deleted, but UPDATE_IN_PROGRESS: Stack2, ROLLBACK_IN_PROGRESS: Stack4"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "sort stacks failure for stacks with some errors (not found, termination protection, and excepted status)",
+			args: args{
+				ctx:        ctx,
+				stackNames: []string{"Stack1", "Stack2", "Stack3", "Stack4", "Stack5"},
+			},
+			prepareMockCloudFormationFn: func(m *client.MockICloudFormation) {
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack1")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack1"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now().Add(-40 * time.Minute)),
+						},
+					},
+					nil,
+				)
 				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack2")).Return(
 					[]types.Stack{
 						{
 							StackName:    aws.String("Stack2"),
+							StackStatus:  types.StackStatusUpdateInProgress,
+							CreationTime: aws.Time(time.Now().Add(-30 * time.Minute)),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack3")).Return(
+					[]types.Stack{
+						{
+							StackName:                   aws.String("Stack3"),
+							StackStatus:                 types.StackStatusCreateComplete,
+							CreationTime:                aws.Time(time.Now().Add(-20 * time.Minute)),
+							EnableTerminationProtection: aws.Bool(true),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack4")).Return(
+					[]types.Stack{},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack5")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack5"),
 							StackStatus:  types.StackStatusCreateComplete,
-							CreationTime: aws.Time(time.Now().Add(-10 * time.Minute)),
+							CreationTime: aws.Time(time.Now()),
+						},
+					},
+					nil,
+				)
+			},
+			want: want{
+				sortedStackNames: []string{},
+				err:              fmt.Errorf("NotExistsError: Stack4 not found"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "sort stacks failure for stacks with some errors (not found and excepted status)",
+			args: args{
+				ctx:        ctx,
+				stackNames: []string{"Stack1", "Stack2", "Stack3", "Stack4", "Stack5"},
+			},
+			prepareMockCloudFormationFn: func(m *client.MockICloudFormation) {
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack1")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack1"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now().Add(-40 * time.Minute)),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack2")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack2"),
+							StackStatus:  types.StackStatusUpdateInProgress,
+							CreationTime: aws.Time(time.Now().Add(-30 * time.Minute)),
 						},
 					},
 					nil,
@@ -1688,7 +1929,21 @@ func TestCloudFormationStackOperator_GetSortedStackNames(t *testing.T) {
 					[]types.Stack{
 						{
 							StackName:    aws.String("Stack3"),
-							StackStatus:  types.StackStatusRollbackInProgress,
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now().Add(-20 * time.Minute)),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack4")).Return(
+					[]types.Stack{},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack5")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack5"),
+							StackStatus:  types.StackStatusCreateComplete,
 							CreationTime: aws.Time(time.Now()),
 						},
 					},
@@ -1697,29 +1952,121 @@ func TestCloudFormationStackOperator_GetSortedStackNames(t *testing.T) {
 			},
 			want: want{
 				sortedStackNames: []string{},
-				err:              fmt.Errorf("OperationInProgressError: Stacks with XxxInProgress cannot be deleted, but UPDATE_IN_PROGRESS: Stack1, ROLLBACK_IN_PROGRESS: Stack3"),
+				err:              fmt.Errorf("NotExistsError: Stack4 not found"),
 			},
 			wantErr: true,
 		},
 		{
-			name: "sort stacks failure for non existent stacks",
+			name: "sort stacks failure for stacks with some errors (not found and termination protection)",
 			args: args{
 				ctx:        ctx,
-				stackNames: []string{"Stack1", "Stack2", "Stack3"},
+				stackNames: []string{"Stack1", "Stack2", "Stack3", "Stack4", "Stack5"},
 			},
 			prepareMockCloudFormationFn: func(m *client.MockICloudFormation) {
 				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack1")).Return(
-					[]types.Stack{},
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack1"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now().Add(-40 * time.Minute)),
+						},
+					},
 					nil,
 				)
 				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack2")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack2"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now().Add(-30 * time.Minute)),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack3")).Return(
+					[]types.Stack{
+						{
+							StackName:                   aws.String("Stack3"),
+							StackStatus:                 types.StackStatusCreateComplete,
+							CreationTime:                aws.Time(time.Now().Add(-20 * time.Minute)),
+							EnableTerminationProtection: aws.Bool(true),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack4")).Return(
 					[]types.Stack{},
 					nil,
 				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack5")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack5"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now()),
+						},
+					},
+					nil,
+				)
+			},
+			want: want{
+				sortedStackNames: []string{},
+				err:              fmt.Errorf("NotExistsError: Stack4 not found"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "sort stacks failure for stacks with some errors (termination protection and excepted status)",
+			args: args{
+				ctx:        ctx,
+				stackNames: []string{"Stack1", "Stack2", "Stack3", "Stack4", "Stack5"},
+			},
+			prepareMockCloudFormationFn: func(m *client.MockICloudFormation) {
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack1")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack1"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now().Add(-40 * time.Minute)),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack2")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack2"),
+							StackStatus:  types.StackStatusUpdateInProgress,
+							CreationTime: aws.Time(time.Now().Add(-30 * time.Minute)),
+						},
+					},
+					nil,
+				)
 				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack3")).Return(
 					[]types.Stack{
 						{
-							StackName:    aws.String("Stack3"),
+							StackName:                   aws.String("Stack3"),
+							StackStatus:                 types.StackStatusCreateComplete,
+							CreationTime:                aws.Time(time.Now().Add(-20 * time.Minute)),
+							EnableTerminationProtection: aws.Bool(true),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack4")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack4"),
+							StackStatus:  types.StackStatusCreateComplete,
+							CreationTime: aws.Time(time.Now().Add(-10 * time.Minute)),
+						},
+					},
+					nil,
+				)
+				m.EXPECT().DescribeStacks(gomock.Any(), aws.String("Stack5")).Return(
+					[]types.Stack{
+						{
+							StackName:    aws.String("Stack5"),
 							StackStatus:  types.StackStatusCreateComplete,
 							CreationTime: aws.Time(time.Now()),
 						},
@@ -1729,7 +2076,7 @@ func TestCloudFormationStackOperator_GetSortedStackNames(t *testing.T) {
 			},
 			want: want{
 				sortedStackNames: []string{},
-				err:              fmt.Errorf("NotExistsError: Stack1, Stack2 stack not found."),
+				err:              fmt.Errorf("TerminationProtectionError: Stack3"),
 			},
 			wantErr: true,
 		},
@@ -1900,7 +2247,7 @@ func TestCloudFormationStackOperator_ListStacksFilteredByKeyword(t *testing.T) {
 			},
 			want: want{
 				filteredStacks: []string{},
-				err:            fmt.Errorf("NotExistsError: No stacks matching the keyword (TestStack)."),
+				err:            fmt.Errorf("NotExistsError: No stacks matching the keyword (TestStack)"),
 			},
 			wantErr: true,
 		},
@@ -2005,7 +2352,7 @@ func TestCloudFormationStackOperator_ListStacksFilteredByKeyword(t *testing.T) {
 			},
 			want: want{
 				filteredStacks: []string{},
-				err:            fmt.Errorf("NotExistsError: No stacks matching the keyword (TestStack)."),
+				err:            fmt.Errorf("NotExistsError: No stacks matching the keyword (TestStack)"),
 			},
 			wantErr: true,
 		},
@@ -2020,7 +2367,7 @@ func TestCloudFormationStackOperator_ListStacksFilteredByKeyword(t *testing.T) {
 			},
 			want: want{
 				filteredStacks: []string{},
-				err:            fmt.Errorf("NotExistsError: No stacks matching the keyword ()."),
+				err:            fmt.Errorf("NotExistsError: No stacks matching the keyword ()"),
 			},
 			wantErr: true,
 		},
@@ -2077,7 +2424,7 @@ func TestCloudFormationStackOperator_ListStacksFilteredByKeyword(t *testing.T) {
 			},
 			want: want{
 				filteredStacks: []string{},
-				err:            fmt.Errorf("NotExistsError: No stacks matching the keyword (TestStack)."),
+				err:            fmt.Errorf("NotExistsError: No stacks matching the keyword (TestStack)"),
 			},
 			wantErr: true,
 		},
@@ -2137,7 +2484,7 @@ func TestCloudFormationStackOperator_ListStacksFilteredByKeyword(t *testing.T) {
 			},
 			want: want{
 				filteredStacks: []string{},
-				err:            fmt.Errorf("NotExistsError: No stacks matching the keyword (TestStack)."),
+				err:            fmt.Errorf("NotExistsError: No stacks matching the keyword (TestStack)"),
 			},
 			wantErr: true,
 		},