diff --git a/.autover/changes/d5f415cd-080b-41e1-8256-8743f7a423d3.json b/.autover/changes/d5f415cd-080b-41e1-8256-8743f7a423d3.json new file mode 100644 index 00000000..9fed55a7 --- /dev/null +++ b/.autover/changes/d5f415cd-080b-41e1-8256-8743f7a423d3.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "AWS.Deploy.CLI", + "Type": "Patch", + "ChangelogMessages": [ + "Fixed intermittent IAM permission failures in delete-deployment command by ensuring CloudFormation client is created after AWS credentials are configured" + ] + } + ] +} \ No newline at end of file diff --git a/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs b/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs index 29e0b929..572bb5f9 100644 --- a/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs +++ b/src/AWS.Deploy.CLI/Commands/DeleteDeploymentCommand.cs @@ -1,10 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Amazon; using Amazon.CloudFormation; using Amazon.CloudFormation.Model; @@ -28,7 +24,6 @@ public class DeleteDeploymentCommand : CancellableAsyncCommand(); _localUserSettingsEngine = localUserSettingsEngine; _awsUtilities = awsUtilities; _projectParserUtility = projectParserUtility; @@ -123,10 +117,7 @@ public override async Task ExecuteAsync(CommandContext context, DeleteDeplo try { - await _cloudFormationClient.DeleteStackAsync(new DeleteStackRequest - { - StackName = settings.DeploymentName - }); + await _awsResourceQueryer.DeleteStack(settings.DeploymentName); // Fire and forget the monitor // Monitor updates the stdout with current status of the CloudFormation stack @@ -218,20 +209,17 @@ private async Task WaitForStackDelete(string stackName) { await Task.Delay(waitTime); - var response = await _cloudFormationClient.DescribeStacksAsync(new DescribeStacksRequest - { - StackName = stackName - }); + var response = await _awsResourceQueryer.DescribeStacks(stackName); - stack = response.Stacks == null || response.Stacks.Count == 0 ? null : response.Stacks[0]; + stack = response.Count == 0 ? null : response[0]; shouldRetry = false; } - catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("ValidationError") && exception.Message.Equals($"Stack with id {stackName} does not exist")) + catch (ResourceQueryException exception) when (exception.InnerException is AmazonCloudFormationException amazonCloudFormationException && amazonCloudFormationException.ErrorCode.Equals("ValidationError") && amazonCloudFormationException.Message.Equals($"Stack with id {stackName} does not exist")) { _interactiveService.WriteDebugLine(exception.PrettyPrint()); shouldRetry = false; } - catch (AmazonCloudFormationException exception) when (exception.ErrorCode.Equals("Throttling")) + catch (ResourceQueryException exception) when (exception.InnerException is AmazonCloudFormationException amazonCloudFormationException && amazonCloudFormationException.ErrorCode.Equals("Throttling")) { _interactiveService.WriteDebugLine(exception.PrettyPrint()); shouldRetry = true; diff --git a/src/AWS.Deploy.CLI/ServerMode/Services/SessionAWSResourceQuery.cs b/src/AWS.Deploy.CLI/ServerMode/Services/SessionAWSResourceQuery.cs index 00ad5ed0..1e3a95ec 100644 --- a/src/AWS.Deploy.CLI/ServerMode/Services/SessionAWSResourceQuery.cs +++ b/src/AWS.Deploy.CLI/ServerMode/Services/SessionAWSResourceQuery.cs @@ -243,6 +243,16 @@ public async Task GetCloudFrontDistribution(string distributionId) return (await GetAndCache(async () => await _awsResourceQueryer.GetCloudFrontDistribution(distributionId), new object[] { distributionId }))!; } + public async Task> DescribeStacks(string stackName) + { + return (await GetAndCache(async () => await _awsResourceQueryer.DescribeStacks(stackName), new object[] { stackName }))!; + } + + public async Task DeleteStack(string stackName) + { + return (await GetAndCache(async () => await _awsResourceQueryer.DeleteStack(stackName), new object[] { stackName }))!; + } + /// public async Task GetDefaultVpc() { diff --git a/src/AWS.Deploy.Common/Data/IAWSResourceQueryer.cs b/src/AWS.Deploy.Common/Data/IAWSResourceQueryer.cs index d361894f..0efaa2f6 100644 --- a/src/AWS.Deploy.Common/Data/IAWSResourceQueryer.cs +++ b/src/AWS.Deploy.Common/Data/IAWSResourceQueryer.cs @@ -36,6 +36,8 @@ public enum BeanstalkPlatformType { Linux, Windows } /// public interface IAWSResourceQueryer { + Task> DescribeStacks(string stackName); + Task DeleteStack(string stackName); Task GetDefaultVpc(); Task GetCloudControlApiResource(string type, string identifier); Task> GetCloudFormationStackEvents(string stackName); diff --git a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs index 4864afe6..cbe6d8e2 100644 --- a/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs +++ b/src/AWS.Deploy.Orchestration/Data/AWSResourceQueryer.cs @@ -51,6 +51,34 @@ namespace AWS.Deploy.Orchestration.Data { public class AWSResourceQueryer(IAWSClientFactory awsClientFactory) : IAWSResourceQueryer { + public async Task> DescribeStacks(string stackName) + { + var cfClient = awsClientFactory.GetAWSClient(); + var request = new DescribeStacksRequest + { + StackName = stackName + }; + + return await HandleException(async () => await cfClient.Paginators + .DescribeStacks(request) + .Stacks + .ToListAsync(), + $"Error attempting to describe available CloudFormation stacks using stack name '{stackName}'"); + } + + public async Task DeleteStack(string stackName) + { + var cfClient = awsClientFactory.GetAWSClient(); + var request = new DeleteStackRequest + { + StackName = stackName + }; + + return await HandleException(async () => await cfClient + .DeleteStackAsync(request), + $"Error attempting to delete the CloudFormation stack '{stackName}'."); + } + public async Task GetCloudControlApiResource(string type, string identifier) { var cloudControlApiClient = awsClientFactory.GetAWSClient(); diff --git a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs index f9a908ca..14abef97 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/Utilities/TestToolAWSResourceQueryer.cs @@ -69,6 +69,10 @@ public Task GetLatestElasticBeanstalkPlatformArn(string? target public Task> DescribeSecurityGroups(string? vpcID = null) => throw new NotImplementedException(); public Task GetParameterStoreTextValue(string parameterName) => throw new NotImplementedException(); public Task GetCloudControlApiResource(string type, string identifier) => throw new NotImplementedException(); + public Task> DescribeStacks(string stackName) => throw new NotImplementedException(); + + public Task DeleteStack(string stackName) => throw new NotImplementedException(); + public Task GetDefaultVpc() => throw new NotImplementedException(); public Task> DescribeElasticBeanstalkConfigurationSettings(string applicationName, string environmentName) => throw new NotImplementedException(); } diff --git a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs index 6389616c..f15bf2e3 100644 --- a/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs +++ b/test/AWS.Deploy.CLI.UnitTests/Utilities/TestToolAWSResourceQueryer.cs @@ -94,6 +94,10 @@ public Task GetLatestElasticBeanstalkPlatformArn(string? target public Task> DescribeSecurityGroups(string? vpcID = null) => throw new NotImplementedException(); public Task GetParameterStoreTextValue(string parameterName) => throw new NotImplementedException(); public Task GetCloudControlApiResource(string type, string identifier) => throw new NotImplementedException(); + public Task> DescribeStacks(string stackName) => throw new NotImplementedException(); + + public Task DeleteStack(string stackName) => throw new NotImplementedException(); + public Task GetDefaultVpc() => throw new NotImplementedException(); public Task> DescribeElasticBeanstalkConfigurationSettings(string applicationName, string environmentName) => throw new NotImplementedException(); }