// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 using System; using System.Linq; using System.Threading.Tasks; using Amazon.CloudFormation; using Amazon.CloudFormation.Model; using AWS.Deploy.CLI.CloudFormation; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; using AWS.Deploy.Orchestration; using AWS.Deploy.Orchestration.LocalUserSettings; namespace AWS.Deploy.CLI.Commands { /// /// Represents a Delete command allows to delete a CloudFormation stack /// public class DeleteDeploymentCommand { private static readonly TimeSpan s_pollingPeriod = TimeSpan.FromSeconds(5); private readonly IAWSClientFactory _awsClientFactory; private readonly IToolInteractiveService _interactiveService; private readonly IAmazonCloudFormation _cloudFormationClient; private readonly IConsoleUtilities _consoleUtilities; private readonly ILocalUserSettingsEngine _localUserSettingsEngine; private readonly OrchestratorSession? _session; private const int MAX_RETRIES = 4; public DeleteDeploymentCommand( IAWSClientFactory awsClientFactory, IToolInteractiveService interactiveService, IConsoleUtilities consoleUtilities, ILocalUserSettingsEngine localUserSettingsEngine, OrchestratorSession? session) { _awsClientFactory = awsClientFactory; _interactiveService = interactiveService; _consoleUtilities = consoleUtilities; _cloudFormationClient = _awsClientFactory.GetAWSClient(); _localUserSettingsEngine = localUserSettingsEngine; _session = session; } /// /// Deletes given CloudFormation stack /// /// The stack name to be deleted /// Thrown when deletion fails public async Task ExecuteAsync(string stackName) { var canDelete = await CanDeleteAsync(stackName); if (!canDelete) { return; } var confirmDelete = _interactiveService.DisableInteractive ? YesNo.Yes : _consoleUtilities.AskYesNoQuestion($"Are you sure you want to delete {stackName}?", YesNo.No); if (confirmDelete == YesNo.No) { return; } _interactiveService.WriteLine($"{stackName}: deleting..."); var monitor = new StackEventMonitor(stackName, _awsClientFactory, _consoleUtilities, _interactiveService); try { await _cloudFormationClient.DeleteStackAsync(new DeleteStackRequest { StackName = stackName }); // Fire and forget the monitor // Monitor updates the stdout with current status of the CloudFormation stack var _ = monitor.StartAsync(); await WaitForStackDelete(stackName); if (_session != null) { await _localUserSettingsEngine.DeleteLastDeployedStack(stackName, _session.ProjectDefinition.ProjectName, _session.AWSAccountId, _session.AWSRegion); } _interactiveService.WriteLine($"{stackName}: deleted"); } finally { // Stop monitoring CloudFormation stack status once the deletion operation finishes monitor.Stop(); } } private async Task CanDeleteAsync(string stackName) { var stack = await GetStackAsync(stackName); if (stack == null) { _interactiveService.WriteErrorLine($"Stack with name {stackName} does not exist."); return false; } var canDelete = stack.Tags.Any(tag => tag.Key.Equals(Constants.CloudFormationIdentifier.STACK_TAG)); if (!canDelete) { _interactiveService.WriteErrorLine("Only stacks that were deployed with this tool can be deleted."); } return canDelete; } private async Task WaitForStackDelete(string stackName) { var stack = await StabilizeStack(stackName); if (stack == null) { return; } if (stack.StackStatus.IsDeleted()) { return; } if (stack.StackStatus.IsFailed()) { throw new FailedToDeleteException(DeployToolErrorCode.FailedToDeleteStack, $"The stack {stackName} is in a failed state. You may need to delete it from the AWS Console."); } throw new FailedToDeleteException(DeployToolErrorCode.FailedToDeleteStack, $"Failed to delete {stackName} stack: {stack.StackStatus}"); } private async Task StabilizeStack(string stackName) { Stack? stack; do { stack = await GetStackAsync(stackName); if (stack == null) { return null; } await Task.Delay(s_pollingPeriod); } while (stack.StackStatus.IsInProgress()); return stack; } private async Task GetStackAsync(string stackName) { var retryCount = 0; var shouldRetry = false; Stack? stack = null; do { var waitTime = GetWaitTime(retryCount); try { await Task.Delay(waitTime); var response = await _cloudFormationClient.DescribeStacksAsync(new DescribeStacksRequest { StackName = stackName }); stack = response.Stacks.Count == 0 ? null : response.Stacks[0]; shouldRetry = false; } catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("ValidationError") && exception.Message.Equals($"Stack with id {stackName} does not exist")) { _interactiveService.WriteDebugLine(exception.PrettyPrint()); shouldRetry = false; } catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("Throttling")) { _interactiveService.WriteDebugLine(exception.PrettyPrint()); shouldRetry = true; } } while (shouldRetry && retryCount++ < MAX_RETRIES); return stack; } /// /// Returns the next wait interval, in milliseconds, using an exponential backoff algorithm /// Read more here https://docs.aws.amazon.com/general/latest/gr/api-retries.html /// /// /// private static TimeSpan GetWaitTime(int retryCount) { if (retryCount == 0) { return TimeSpan.Zero; } var waitTime = Math.Pow(2, retryCount) * 5; return TimeSpan.FromSeconds(waitTime); } } }