Repository: gruntwork-io/cloud-nuke
Pull request #656 (2024-03-26 21:47:48)

implementation ec2 internet gateway nuke
<!-- Prepend '[WIP]' to the title if this PR is still a work-in-progress. Remove it when it is ready for review! -->

## Description

Partially Fixing https://github.com/gruntwork-io/cloud-nuke/issues/476

<!-- 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/README.md b/README.md
index 1cd9f04..e82338f 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,5 @@
 [![Maintained by Gruntwork.io](https://img.shields.io/badge/maintained%20by-gruntwork.io-%235849a6.svg)](https://gruntwork.io/?ref=repo_cloud_nuke)
 
-## Telemetry
-
-As of version `v0.29.0` cloud-nuke sends telemetry back to Gruntwork to help us better prioritize bug fixes and feature
-improvements. The following metrics are included:
-
-- Command and Arguments
-- Version Number
-- Timestamps
-- Resource Types
-- Resource Counts
-- A randomly generated Run ID
-- AWS Account ID
-
-We never collect
-
-- IP Addresses
-- Resource Names
-
-Telemetry can be disabled entirely by setting the `DISABLE_TELEMETRY` environment variable on the command line.
-
-As an open source tool, you can see the exact statistics being collected by searching the code for
-`telemetry.TrackEvent(...)`
-
 # cloud-nuke
 
 This repo contains a CLI tool to delete all resources . cloud-nuke was created for situations when you might have an
@@ -55,6 +32,7 @@ Cloud-nuke suppports 🔎 inspecting and 🔥💀 deleting the following AWS res
 | EC2                     | IPAM Custom Allocation 								                   |
 | EC2                     | IPAM BYOASN	          								                   |
 | EC2                     | IPAM Resource Discovery 							                   |
+| EC2                     | Internet Gateway        							                   |
 | Certificate Manager     | ACM Private CA                                           |
 | Direct Connect          | Transit Gateways                                         |
 | Elasticache             | Clusters                                                 |
@@ -136,6 +114,29 @@ be used in a production environment!
 When executed as `cloud-nuke defaults-aws`, this tool deletes all DEFAULT VPCs and the default ingress/egress rule for
 all default security groups. This should be used in production environments **WITH CAUTION**.
 
+## Telemetry
+
+As of version `v0.29.0` cloud-nuke sends telemetry back to Gruntwork to help us better prioritize bug fixes and feature
+improvements. The following metrics are included:
+
+- Command and Arguments
+- Version Number
+- Timestamps
+- Resource Types
+- Resource Counts
+- A randomly generated Run ID
+- AWS Account ID
+
+We never collect
+
+- IP Addresses
+- Resource Names
+
+Telemetry can be disabled entirely by setting the `DISABLE_TELEMETRY` environment variable on the command line.
+
+As an open source tool, you can see the exact statistics being collected by searching the code for
+`telemetry.TrackEvent(...)`
+
 ## Install
 
 ### Download from releases page
@@ -565,6 +566,7 @@ of the file that are supported are listed here.
 | iam-role                    | IAMRoles                     | ✅ (Role Name)                         | ✅ (Creation Time)                   | ❌    |    ❌   |
 | iam-service-linked-role     | IAMServiceLinkedRoles        | ✅ (Service Linked Role Name)          | ✅ (Creation Time)                   | ❌    |    ❌   |
 | iam                         | IAMUsers                     | ✅ (User Name)                         | ✅ (Creation Time)                   | ✅    |    ❌   |
+| internet-gateway            | InternetGateway              | ✅ (Gateway Name)                      | ✅ (Creation Time)                   | ✅    |    ❌   |
 | kmscustomerkeys             | KMSCustomerKeys              | ✅ (Key Name)                          | ✅ (Creation Time)                   | ❌    |    ❌   |
 | kinesis-stream              | KinesisStream                | ✅ (Stream Name)                       | ❌                                   | ❌    |    ❌   |
 | lambda                      | LambdaFunction               | ✅ (Function Name)                     | ✅ (Last Modified Time)              | ❌    |    ❌   |
@@ -603,6 +605,7 @@ of the file that are supported are listed here.
 | route53-cidr-collection     | Route53CIDRCollection        | ✅ (Cidr collection name)              | ❌                                   | ❌    |    ❌   |
 | route53-traffic-policy      | Route53TrafficPolicy         | ✅ (Traffic policy name)               | ❌                                   | ❌    |    ❌   |
 
+
 ### How to Use
 
 Once you created your config file, you can run a command like this to nuke resources with your config file:
diff --git a/aws/resource_registry.go b/aws/resource_registry.go
index c2c2e98..283b22b 100644
--- a/aws/resource_registry.go
+++ b/aws/resource_registry.go
@@ -127,6 +127,7 @@ func getRegisteredRegionalResources() []AwsResource {
 		&resources.Route53HostedZone{},
 		&resources.Route53CidrCollection{},
 		&resources.Route53TrafficPolicy{},
+		&resources.InternetGateway{},
 	}
 }
 
diff --git a/aws/resources/ec2_internet_gateway.go b/aws/resources/ec2_internet_gateway.go
new file mode 100644
index 0000000..5debb9d
--- /dev/null
+++ b/aws/resources/ec2_internet_gateway.go
@@ -0,0 +1,201 @@
+package resources
+
+import (
+	"context"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/cloud-nuke/logging"
+	r "github.com/gruntwork-io/cloud-nuke/report" // Alias the package as 'r'
+	"github.com/gruntwork-io/cloud-nuke/util"
+	"github.com/gruntwork-io/go-commons/errors"
+)
+
+func (igw *InternetGateway) setFirstSeenTag(gateway ec2.InternetGateway, value time.Time) error {
+	_, err := igw.Client.CreateTags(&ec2.CreateTagsInput{
+		Resources: []*string{gateway.InternetGatewayId},
+		Tags: []*ec2.Tag{
+			{
+				Key:   aws.String(util.FirstSeenTagKey),
+				Value: aws.String(util.FormatTimestamp(value)),
+			},
+		},
+	})
+	if err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
+
+func (igw *InternetGateway) getFirstSeenTag(gateway ec2.InternetGateway) (*time.Time, error) {
+	tags := gateway.Tags
+	for _, tag := range tags {
+		if util.IsFirstSeenTag(tag.Key) {
+			firstSeenTime, err := util.ParseTimestamp(tag.Value)
+			if err != nil {
+				return nil, errors.WithStackTrace(err)
+			}
+
+			return firstSeenTime, nil
+		}
+	}
+
+	return nil, nil
+}
+
+func shouldIncludeGateway(ig *ec2.InternetGateway, firstSeenTime *time.Time, configObj config.Config) bool {
+	var internetGateway string
+	// get the tags as map
+	tagMap := util.ConvertEC2TagsToMap(ig.Tags)
+	if name, ok := tagMap["Name"]; ok {
+		internetGateway = name
+	}
+
+	return configObj.InternetGateway.ShouldInclude(config.ResourceValue{
+		Name: &internetGateway,
+		Tags: tagMap,
+		Time: firstSeenTime,
+	})
+}
+
+func (igw *InternetGateway) getAll(_ context.Context, configObj config.Config) ([]*string, error) {
+	var identifiers []*string
+
+	input := &ec2.DescribeInternetGatewaysInput{}
+	resp, err := igw.Client.DescribeInternetGateways(input)
+	if err != nil {
+		logging.Debugf("[Internet Gateway] Failed to list internet gateways: %s", err)
+		return nil, err
+	}
+
+	for _, ig := range resp.InternetGateways {
+		// check first seen tag
+		firstSeenTime, err := igw.getFirstSeenTag(*ig)
+		if err != nil {
+			logging.Errorf(
+				"Unable to retrieve tags for Internet gateway: %s, with error: %s", *ig.InternetGatewayId, err)
+			continue
+		}
+
+		// if the first seen tag is not there, then create one
+		if firstSeenTime == nil {
+			now := time.Now().UTC()
+			firstSeenTime = &now
+			if err := igw.setFirstSeenTag(*ig, time.Now().UTC()); err != nil {
+				logging.Errorf(
+					"Unable to apply first seen tag Internet gateway: %s, with error: %s", *ig.InternetGatewayId, err)
+				continue
+			}
+		}
+
+		if shouldIncludeGateway(ig, firstSeenTime, configObj) {
+			identifiers = append(identifiers, ig.InternetGatewayId)
+		}
+	}
+
+	// Check and verify the list of allowed nuke actions
+	igw.VerifyNukablePermissions(identifiers, func(id *string) error {
+		params := &ec2.DeleteInternetGatewayInput{
+			InternetGatewayId: id,
+			DryRun:            aws.Bool(true),
+		}
+		_, err := igw.Client.DeleteInternetGateway(params)
+		return err
+	})
+
+	return identifiers, nil
+}
+
+func (igw *InternetGateway) nuke(id *string) error {
+
+	// detaching the gateway from attached vpcs
+	if err := igw.detachInternetGateway(id); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	// nuking the gateway
+	if err := igw.nukeInternetGateway(id); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
+
+func (igw *InternetGateway) nukeInternetGateway(id *string) error {
+	logging.Debugf("Deleting Internet gateway %s", *id)
+
+	_, err := igw.Client.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{
+		InternetGatewayId: id,
+	})
+	if err != nil {
+		logging.Debugf("[Failed] Error deleting internet gateway %s: %s", *id, err)
+		return errors.WithStackTrace(err)
+	}
+	logging.Debugf("[Ok] internet gateway deleted successfully %s", *id)
+
+	return nil
+}
+
+func (igw *InternetGateway) detachInternetGateway(id *string) error {
+	logging.Debugf("Detaching Internet gateway %s", *id)
+
+	output, err := igw.Client.DescribeInternetGateways(&ec2.DescribeInternetGatewaysInput{
+		InternetGatewayIds: []*string{id},
+	})
+	if err != nil {
+		logging.Debugf("[Failed] Error describing internet gateway %s: %s", *id, err)
+		return errors.WithStackTrace(err)
+	}
+
+	for _, gateway := range output.InternetGateways {
+		for _, attachments := range gateway.Attachments {
+			_, err := igw.Client.DetachInternetGateway(&ec2.DetachInternetGatewayInput{
+				InternetGatewayId: id,
+				VpcId:             attachments.VpcId,
+			})
+			if err != nil {
+				logging.Debugf("[Failed] Error detaching internet gateway %s: %s", *id, err)
+				return errors.WithStackTrace(err)
+			}
+		}
+	}
+	logging.Debugf("[Ok] internet gateway detached successfully %s", *id)
+	return nil
+}
+
+func (igw *InternetGateway) nukeAll(identifiers []*string) error {
+	if len(identifiers) == 0 {
+		logging.Debugf("No internet gateway identifiers to nuke in region %s", igw.Region)
+		return nil
+	}
+
+	logging.Debugf("Deleting all internet gateways in region %s", igw.Region)
+	var deletedGateways []*string
+
+	for _, id := range identifiers {
+		if nukable, err := igw.IsNukable(*id); !nukable {
+			logging.Debugf("[Skipping] %s nuke because %v", *id, err)
+			continue
+		}
+
+		err := igw.nuke(id)
+		// Record status of this resource
+		e := r.Entry{ // Use the 'r' alias to refer to the package
+			Identifier:   aws.StringValue(id),
+			ResourceType: "Internet Gateway",
+			Error:        err,
+		}
+		r.Record(e)
+
+		if err == nil {
+			deletedGateways = append(deletedGateways, id)
+		}
+	}
+
+	logging.Debugf("[OK] %d internet gateway(s) deleted in %s", len(deletedGateways), igw.Region)
+
+	return nil
+}
diff --git a/aws/resources/ec2_internet_gateway_types.go b/aws/resources/ec2_internet_gateway_types.go
new file mode 100644
index 0000000..3c1fb80
--- /dev/null
+++ b/aws/resources/ec2_internet_gateway_types.go
@@ -0,0 +1,54 @@
+package resources
+
+import (
+	"context"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/go-commons/errors"
+)
+
+type InternetGateway struct {
+	BaseAwsResource
+	Client     ec2iface.EC2API
+	Region     string
+	GatewayIds []string
+}
+
+func (igw *InternetGateway) Init(session *session.Session) {
+	igw.BaseAwsResource.Init(session)
+	igw.Client = ec2.New(session)
+}
+
+func (igw *InternetGateway) ResourceName() string {
+	return "internet-gateway"
+}
+
+func (igw *InternetGateway) ResourceIdentifiers() []string {
+	return igw.GatewayIds
+}
+
+func (igw *InternetGateway) MaxBatchSize() int {
+	return 50
+}
+
+func (igw *InternetGateway) GetAndSetIdentifiers(c context.Context, configObj config.Config) ([]string, error) {
+	identifiers, err := igw.getAll(c, configObj)
+	if err != nil {
+		return nil, err
+	}
+
+	igw.GatewayIds = aws.StringValueSlice(identifiers)
+	return igw.GatewayIds, nil
+}
+
+func (igw *InternetGateway) Nuke(identifiers []string) error {
+	if err := igw.nukeAll(aws.StringSlice(identifiers)); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
diff --git a/config/config.go b/config/config.go
index 9ea7c3e..fee881e 100644
--- a/config/config.go
+++ b/config/config.go
@@ -98,6 +98,7 @@ type Config struct {
 	Route53HostedZone               ResourceType               `yaml:"Route53HostedZone"`
 	Route53CIDRCollection           ResourceType               `yaml:"Route53CIDRCollection"`
 	Route53TrafficPolicy            ResourceType               `yaml:"Route53TrafficPolicy"`
+	InternetGateway                 ResourceType               `yaml:"InternetGateway"`
 }
 
 func (c *Config) addTimeAfterFilter(timeFilter *time.Time, fieldName string) {


Test patch:
diff --git a/aws/resources/ec2_internet_gateway_test.go b/aws/resources/ec2_internet_gateway_test.go
new file mode 100644
index 0000000..8b0efb7
--- /dev/null
+++ b/aws/resources/ec2_internet_gateway_test.go
@@ -0,0 +1,169 @@
+package resources
+
+import (
+	"context"
+	"regexp"
+	"testing"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/cloud-nuke/telemetry"
+	"github.com/gruntwork-io/cloud-nuke/util"
+	"github.com/stretchr/testify/require"
+)
+
+type mockedInternetGateway struct {
+	BaseAwsResource
+	ec2iface.EC2API
+	DescribeInternetGatewaysOutput ec2.DescribeInternetGatewaysOutput
+	DetachInternetGatewayOutput    ec2.DetachInternetGatewayOutput
+	DeleteInternetGatewayOutput    ec2.DeleteInternetGatewayOutput
+}
+
+func (m mockedInternetGateway) DescribeInternetGateways(_ *ec2.DescribeInternetGatewaysInput) (*ec2.DescribeInternetGatewaysOutput, error) {
+	return &m.DescribeInternetGatewaysOutput, nil
+}
+
+func (m mockedInternetGateway) DetachInternetGateway(_ *ec2.DetachInternetGatewayInput) (*ec2.DetachInternetGatewayOutput, error) {
+	return &m.DetachInternetGatewayOutput, nil
+}
+
+func (m mockedInternetGateway) DeleteInternetGateway(_ *ec2.DeleteInternetGatewayInput) (*ec2.DeleteInternetGatewayOutput, error) {
+	return &m.DeleteInternetGatewayOutput, nil
+}
+
+func TestEc2InternetGateway_GetAll(t *testing.T) {
+	telemetry.InitTelemetry("cloud-nuke", "")
+	t.Parallel()
+
+	var (
+		now      = time.Now()
+		gateway1 = "igw-0b44cfa6103932e1d001"
+		gateway2 = "igw-0b44cfa6103932e1d002"
+
+		testName1 = "cloud-nuke-igw-001"
+		testName2 = "cloud-nuke-igw-002"
+	)
+
+	igw := InternetGateway{
+		Client: mockedInternetGateway{
+			DescribeInternetGatewaysOutput: ec2.DescribeInternetGatewaysOutput{
+				InternetGateways: []*ec2.InternetGateway{
+					{
+						InternetGatewayId: aws.String(gateway1),
+						Tags: []*ec2.Tag{
+							{
+								Key:   aws.String("Name"),
+								Value: aws.String(testName1),
+							}, {
+								Key:   aws.String(util.FirstSeenTagKey),
+								Value: aws.String(util.FormatTimestamp(now.Add(1))),
+							},
+						},
+					},
+					{
+						InternetGatewayId: aws.String(gateway2),
+						Tags: []*ec2.Tag{
+							{
+								Key:   aws.String("Name"),
+								Value: aws.String(testName2),
+							}, {
+								Key:   aws.String(util.FirstSeenTagKey),
+								Value: aws.String(util.FormatTimestamp(now.Add(1))),
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	igw.BaseAwsResource.Init(nil)
+
+	tests := map[string]struct {
+		configObj config.ResourceType
+		expected  []string
+	}{
+		"emptyFilter": {
+			configObj: config.ResourceType{},
+			expected:  []string{gateway1, gateway2},
+		},
+		"nameExclusionFilter": {
+			configObj: config.ResourceType{
+				ExcludeRule: config.FilterRule{
+					NamesRegExp: []config.Expression{{
+						RE: *regexp.MustCompile(testName1),
+					}}},
+			},
+			expected: []string{gateway2},
+		},
+		"timeAfterExclusionFilter": {
+			configObj: config.ResourceType{
+				ExcludeRule: config.FilterRule{
+					TimeAfter: aws.Time(now.Add(-1 * time.Hour)),
+				}},
+			expected: []string{},
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			names, err := igw.getAll(context.Background(), config.Config{
+				InternetGateway: tc.configObj,
+			})
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, aws.StringValueSlice(names))
+		})
+	}
+
+}
+
+func TestEc2InternetGateway_NukeAll(t *testing.T) {
+	telemetry.InitTelemetry("cloud-nuke", "")
+	t.Parallel()
+
+	var (
+		gateway1 = "igw-0b44cfa6103932e1d001"
+		gateway2 = "igw-0b44cfa6103932e1d002"
+	)
+
+	igw := InternetGateway{
+		BaseAwsResource: BaseAwsResource{
+			Nukables: map[string]error{
+				gateway1: nil,
+			},
+		},
+		Client: mockedInternetGateway{
+			DescribeInternetGatewaysOutput: ec2.DescribeInternetGatewaysOutput{
+				InternetGateways: []*ec2.InternetGateway{
+					{
+						InternetGatewayId: aws.String(gateway1),
+						Attachments: []*ec2.InternetGatewayAttachment{
+							{
+								State: aws.String("testing-state"),
+								VpcId: aws.String("test-gateway-vpc"),
+							},
+						},
+					},
+					{
+						InternetGatewayId: aws.String(gateway2),
+						Attachments: []*ec2.InternetGatewayAttachment{
+							{
+								State: aws.String("testing-state"),
+								VpcId: aws.String("test-gateway-vpc"),
+							},
+						},
+					},
+				},
+			},
+			DeleteInternetGatewayOutput: ec2.DeleteInternetGatewayOutput{},
+		},
+	}
+
+	err := igw.nukeAll([]*string{
+		aws.String(gateway1),
+		aws.String(gateway2),
+	})
+	require.NoError(t, err)
+}
diff --git a/config/config_test.go b/config/config_test.go
index 614ef47..d03ccd4 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -96,6 +96,7 @@ func emptyConfig() *Config {
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
 		ResourceType{FilterRule{}, FilterRule{}, ""},
+		ResourceType{FilterRule{}, FilterRule{}, ""},
 	}
 }