Repository: gruntwork-io/cloud-nuke
Pull request #595 (2023-09-29 22:05:51)

Remove associated Api mapping before deleting Api Gateway V2
<!-- Prepend '[WIP]' to the title if this PR is still a work-in-progress. Remove it when it is ready for review! -->

## Description

Fixes https://github.com/gruntwork-io/cloud-nuke/issues/594.

Successfully deleted all Api Gateway in phxdevops
<img width="500" alt="image" src="https://github.com/gruntwork-io/cloud-nuke/assets/96548424/c8dfa7f0-092d-4254-ac0d-8f09088fd9a7">


<!-- Description of the changes introduced by this PR. -->

## TODOs

Read the [Gruntwork contribution guidelines](https://gruntwork.notion.site/Gruntwork-Coding-Methodology-02fdcd6e4b004e818553684760bf691e).

- [ ] Update the docs.
- [ ] Run the relevant tests successfully, including pre-commit checks.
- [ ] Ensure any 3rd party code adheres with our [license policy](https://www.notion.so/gruntwork/Gruntwork-licenses-and-open-source-usage-policy-f7dece1f780341c7b69c1763f22b1378) or delete this line if its not applicable.
- [ ] Include release notes. If this PR is backward incompatible, include a migration guide.
- [ ] Attention Grunts - if this PR adds support for a new resource, ensure the `nuke_sandbox` and `nuke_phxdevops` jobs in `.circleci/config.yml` have been updated with appropriate exclusions (either directly in the job or via the `.circleci/nuke_config.yml` file) to prevent nuking IAM roles, groups, resources, etc that are important for the test accounts.


## Release Notes (draft)

<!-- One-line description of the PR that can be included in the final release notes. -->
Added / Removed / Updated [X].

### Migration Guide

<!-- Important: If you made any backward incompatible changes, then you must write a migration guide! -->




Patch:
diff --git a/aws/aws.go b/aws/aws.go
index 7cc8f5a..488830c 100644
--- a/aws/aws.go
+++ b/aws/aws.go
@@ -11,7 +11,6 @@ import (
 
 	commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
 
-	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/gruntwork-io/cloud-nuke/config"
 	"github.com/gruntwork-io/cloud-nuke/logging"
 	"github.com/gruntwork-io/cloud-nuke/progressbar"
@@ -111,7 +110,7 @@ func IsNukeable(resourceType string, resourceTypes []string) bool {
 	return false
 }
 
-func nukeAllResourcesInRegion(account *AwsAccountResources, region string, session *session.Session) {
+func nukeAllResourcesInRegion(account *AwsAccountResources, region string) {
 	resourcesInRegion := account.Resources[region]
 
 	for _, resources := range resourcesInRegion.Resources {
@@ -157,33 +156,17 @@ func NukeAllResources(account *AwsAccountResources, regions []string) error {
 		EventName: "Begin nuking resources",
 	}, map[string]interface{}{})
 
-	defaultRegion := regions[0]
 	for _, region := range regions {
-		// region that will be used to create a session
-		sessionRegion := region
-
-		// As there is no actual region named global we have to pick a valid one just to create the session
-		if region == GlobalRegion {
-			sessionRegion = defaultRegion
-		}
-
 		telemetry.TrackEvent(commonTelemetry.EventContext{
 			EventName: "Creating session for region",
 		}, map[string]interface{}{
 			"region": region,
 		})
-		session := NewSession(sessionRegion)
-		telemetry.TrackEvent(commonTelemetry.EventContext{
-			EventName: "Nuking Region",
-		}, map[string]interface{}{
-			"region":        region,
-			"resourceCount": len(account.Resources[region].Resources),
-		})
 
 		// We intentionally do not handle an error returned from this method, because we collect individual errors
 		// on per-resource basis via the report package's Record method. In the run report displayed at the end of
 		// a cloud-nuke run, we show exactly which resources deleted cleanly and which encountered errors
-		nukeAllResourcesInRegion(account, region, session)
+		nukeAllResourcesInRegion(account, region)
 		telemetry.TrackEvent(commonTelemetry.EventContext{
 			EventName: "Done Nuking Region",
 		}, map[string]interface{}{
diff --git a/aws/resources/apigatewayv2.go b/aws/resources/apigatewayv2.go
index d4e31ad..22c2786 100644
--- a/aws/resources/apigatewayv2.go
+++ b/aws/resources/apigatewayv2.go
@@ -2,6 +2,9 @@ package resources
 
 import (
 	"context"
+	"fmt"
+	"github.com/aws/aws-sdk-go/service/apigatewayv2/apigatewayv2iface"
+	"github.com/pterm/pterm"
 	"sync"
 
 	commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
@@ -9,7 +12,6 @@ import (
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/apigatewayv2"
 	"github.com/gruntwork-io/cloud-nuke/config"
-	"github.com/gruntwork-io/cloud-nuke/logging"
 	"github.com/gruntwork-io/cloud-nuke/report"
 	"github.com/gruntwork-io/cloud-nuke/telemetry"
 	"github.com/gruntwork-io/go-commons/errors"
@@ -37,16 +39,22 @@ func (gw *ApiGatewayV2) getAll(c context.Context, configObj config.Config) ([]*s
 
 func (gw *ApiGatewayV2) nukeAll(identifiers []*string) error {
 	if len(identifiers) == 0 {
-		logging.Logger.Debugf("No API Gateways (v2) to nuke in region %s", gw.Region)
+		pterm.Debug.Println(fmt.Sprintf("No API Gateways (v2) to nuke in region %s", gw.Region))
 	}
 
 	if len(identifiers) > 100 {
-		logging.Logger.Debugf("Nuking too many API Gateways (v2) at once (100): halting to avoid hitting AWS API rate limiting")
+		pterm.Debug.Println(fmt.Sprintf(
+			"Nuking too many API Gateways (v2) at once (100): halting to avoid hitting AWS API rate limiting"))
 		return TooManyApiGatewayV2Err{}
 	}
 
+	err := deleteAssociatedApiMappings(gw.Client, identifiers)
+	if err != nil {
+		return errors.WithStackTrace(err)
+	}
+
 	// There is no bulk delete Api Gateway API, so we delete the batch of gateways concurrently using goroutines
-	logging.Logger.Debugf("Deleting Api Gateways (v2) in region %s", gw.Region)
+	pterm.Debug.Println(fmt.Sprintf("Deleting Api Gateways (v2) in region %s", gw.Region))
 	wg := new(sync.WaitGroup)
 	wg.Add(len(identifiers))
 	errChans := make([]chan error, len(identifiers))
@@ -60,7 +68,6 @@ func (gw *ApiGatewayV2) nukeAll(identifiers []*string) error {
 	for _, errChan := range errChans {
 		if err := <-errChan; err != nil {
 			allErrs = multierror.Append(allErrs, err)
-			logging.Logger.Debugf("[Failed] %s", err)
 			telemetry.TrackEvent(commonTelemetry.EventContext{
 				EventName: "Error Nuking API Gateway V2",
 			}, map[string]interface{}{
@@ -91,8 +98,53 @@ func (gw *ApiGatewayV2) deleteAsync(wg *sync.WaitGroup, errChan chan error, apiI
 	report.Record(e)
 
 	if err == nil {
-		logging.Logger.Debugf("[OK] API Gateway (v2) %s deleted in %s", aws.StringValue(apiId), gw.Region)
+		pterm.Debug.Println(fmt.Sprintf("Successfully deleted API Gateway (v2) %s in %s", aws.StringValue(apiId), gw.Region))
 	} else {
-		logging.Logger.Debugf("[Failed] Error deleting API Gateway (v2) %s in %s", aws.StringValue(apiId), gw.Region)
+		pterm.Debug.Println(fmt.Sprintf("Failed to delete API Gateway (v2) %s in %s", aws.StringValue(apiId), gw.Region))
+	}
+}
+
+func deleteAssociatedApiMappings(
+	client apigatewayv2iface.ApiGatewayV2API, identifiers []*string) error {
+	// Convert identifiers to map to check if identifier is in list
+	identifierMap := make(map[string]bool)
+	for _, identifier := range identifiers {
+		identifierMap[*identifier] = true
 	}
+
+	domainNames, err := client.GetDomainNames(&apigatewayv2.GetDomainNamesInput{})
+	if err != nil {
+		pterm.Debug.Println(fmt.Sprintf("Failed to get domain names: %s", err))
+		return errors.WithStackTrace(err)
+	}
+
+	pterm.Debug.Println(fmt.Sprintf("Found %d domain names", len(domainNames.Items)))
+	for _, domainName := range domainNames.Items {
+		apiMappings, err := client.GetApiMappings(&apigatewayv2.GetApiMappingsInput{
+			DomainName: domainName.DomainName,
+		})
+		if err != nil {
+			pterm.Debug.Println(fmt.Sprintf("Failed to get api mappings: %s", err))
+			return errors.WithStackTrace(err)
+		}
+
+		for _, apiMapping := range apiMappings.Items {
+			if _, ok := identifierMap[*apiMapping.ApiId]; !ok {
+				continue
+			}
+
+			_, err := client.DeleteApiMapping(&apigatewayv2.DeleteApiMappingInput{
+				ApiMappingId: apiMapping.ApiMappingId,
+				DomainName:   domainName.DomainName,
+			})
+			if err != nil {
+				pterm.Debug.Println(fmt.Sprintf("Failed to delete api mapping: %s", err))
+				return errors.WithStackTrace(err)
+			}
+
+			pterm.Debug.Println(fmt.Sprintf("Deleted api mapping: %s", *apiMapping.ApiMappingId))
+		}
+	}
+
+	return nil
 }


Test patch:
diff --git a/aws/resources/apigatewayv2_test.go b/aws/resources/apigatewayv2_test.go
index 28bc7c2..53694d0 100644
--- a/aws/resources/apigatewayv2_test.go
+++ b/aws/resources/apigatewayv2_test.go
@@ -18,8 +18,11 @@ import (
 
 type mockedApiGatewayV2 struct {
 	apigatewayv2iface.ApiGatewayV2API
-	GetApisOutput   apigatewayv2.GetApisOutput
-	DeleteApiOutput apigatewayv2.DeleteApiOutput
+	GetApisOutput          apigatewayv2.GetApisOutput
+	DeleteApiOutput        apigatewayv2.DeleteApiOutput
+	GetDomainNamesOutput   apigatewayv2.GetDomainNamesOutput
+	GetApiMappingsOutput   apigatewayv2.GetApiMappingsOutput
+	DeleteApiMappingOutput apigatewayv2.DeleteApiMappingOutput
 }
 
 func (m mockedApiGatewayV2) GetApis(*apigatewayv2.GetApisInput) (*apigatewayv2.GetApisOutput, error) {
@@ -32,6 +35,18 @@ func (m mockedApiGatewayV2) DeleteApi(*apigatewayv2.DeleteApiInput) (*apigateway
 	return &m.DeleteApiOutput, nil
 }
 
+func (m mockedApiGatewayV2) GetDomainNames(*apigatewayv2.GetDomainNamesInput) (*apigatewayv2.GetDomainNamesOutput, error) {
+	return &m.GetDomainNamesOutput, nil
+}
+
+func (m mockedApiGatewayV2) GetApiMappings(*apigatewayv2.GetApiMappingsInput) (*apigatewayv2.GetApiMappingsOutput, error) {
+	return &m.GetApiMappingsOutput, nil
+}
+
+func (m mockedApiGatewayV2) DeleteApiMapping(*apigatewayv2.DeleteApiMappingInput) (*apigatewayv2.DeleteApiMappingOutput, error) {
+	return &m.DeleteApiMappingOutput, nil
+}
+
 func TestApiGatewayV2GetAll(t *testing.T) {
 	telemetry.InitTelemetry("cloud-nuke", "")
 	t.Parallel()
@@ -84,6 +99,21 @@ func TestApiGatewayV2NukeAll(t *testing.T) {
 	gw := ApiGatewayV2{
 		Client: mockedApiGatewayV2{
 			DeleteApiOutput: apigatewayv2.DeleteApiOutput{},
+			GetDomainNamesOutput: apigatewayv2.GetDomainNamesOutput{
+				Items: []*apigatewayv2.DomainName{
+					{
+						DomainName: aws.String("test-domain-name"),
+					},
+				},
+			},
+			GetApisOutput: apigatewayv2.GetApisOutput{
+				Items: []*apigatewayv2.Api{
+					{
+						ApiId: aws.String("test-api-id"),
+					},
+				},
+			},
+			DeleteApiMappingOutput: apigatewayv2.DeleteApiMappingOutput{},
 		},
 	}
 	err := gw.nukeAll([]*string{aws.String("test-api-id")})