Skip to content

Commit

Permalink
implement egress only internet gateway nuke (#657)
Browse files Browse the repository at this point in the history
  • Loading branch information
james03160927 authored Apr 2, 2024
1 parent 2e34b83 commit 3c3d3fb
Show file tree
Hide file tree
Showing 7 changed files with 385 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Cloud-nuke suppports 🔎 inspecting and 🔥💀 deleting the following AWS res
| EC2 | IPAM BYOASN |
| EC2 | IPAM Resource Discovery |
| EC2 | Internet Gateway |
| EC2 | Egress only internet gateway |
| Certificate Manager | ACM Private CA |
| Direct Connect | Transit Gateways |
| Elasticache | Clusters |
Expand Down Expand Up @@ -568,6 +569,7 @@ of the file that are supported are listed here.
| iam-service-linked-role | IAMServiceLinkedRoles | ✅ (Service Linked Role Name) | ✅ (Creation Time) | ❌ | ❌ |
| iam | IAMUsers | ✅ (User Name) | ✅ (Creation Time) | ✅ | ❌ |
| internet-gateway | InternetGateway | ✅ (Gateway Name) | ✅ (Creation Time) | ✅ | ❌ |
| egress-only-internet-gateway| EgressOnlyInternetGateway | ✅ (Gateway name) | ✅ (Creation Time) | ✅ | ❌ |
| kmscustomerkeys | KMSCustomerKeys | ✅ (Key Name) | ✅ (Creation Time) | ❌ | ❌ |
| kinesis-stream | KinesisStream | ✅ (Stream Name) | ❌ | ❌ | ❌ |
| lambda | LambdaFunction | ✅ (Function Name) | ✅ (Last Modified Time) | ❌ | ❌ |
Expand Down
1 change: 1 addition & 0 deletions aws/resource_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func getRegisteredRegionalResources() []AwsResource {
&resources.ECR{},
&resources.ECSClusters{},
&resources.ECSServices{},
&resources.EgressOnlyInternetGateway{},
&resources.ElasticFileSystem{},
&resources.EIPAddresses{},
&resources.EKSClusters{},
Expand Down
150 changes: 150 additions & 0 deletions aws/resources/ec2_egress_only_igw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
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"
"github.com/gruntwork-io/cloud-nuke/report"
"github.com/gruntwork-io/cloud-nuke/util"
"github.com/gruntwork-io/go-commons/errors"
)

func (egigw *EgressOnlyInternetGateway) setFirstSeenTag(eoig ec2.EgressOnlyInternetGateway, value time.Time) error {
_, err := egigw.Client.CreateTags(&ec2.CreateTagsInput{
Resources: []*string{eoig.EgressOnlyInternetGatewayId},
Tags: []*ec2.Tag{
{
Key: aws.String(util.FirstSeenTagKey),
Value: aws.String(util.FormatTimestamp(value)),
},
},
})
if err != nil {
return errors.WithStackTrace(err)
}

return nil
}

func (egigw *EgressOnlyInternetGateway) getFirstSeenTag(eoig ec2.EgressOnlyInternetGateway) (*time.Time, error) {
tags := eoig.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 shouldIncludeEgressOnlyInternetGateway(gateway *ec2.EgressOnlyInternetGateway, firstSeenTime *time.Time, configObj config.Config) bool {
var gatewayName string
// get the tags as map
tagMap := util.ConvertEC2TagsToMap(gateway.Tags)
if name, ok := tagMap["Name"]; ok {
gatewayName = name
}
return configObj.EgressOnlyInternetGateway.ShouldInclude(config.ResourceValue{
Name: &gatewayName,
Tags: tagMap,
Time: firstSeenTime,
})
}

func (egigw *EgressOnlyInternetGateway) getAll(_ context.Context, configObj config.Config) ([]*string, error) {
var result []*string

output, err := egigw.Client.DescribeEgressOnlyInternetGateways(&ec2.DescribeEgressOnlyInternetGatewaysInput{})
if err != nil {
return nil, errors.WithStackTrace(err)
}

for _, igw := range output.EgressOnlyInternetGateways {
// check first seen tag
firstSeenTime, err := egigw.getFirstSeenTag(*igw)
if err != nil {
logging.Errorf(
"Unable to retrieve tags for Egress IGW: %s, with error: %s", *igw.EgressOnlyInternetGatewayId, err)
continue
}

// if the first seen tag is not there, then create one
if firstSeenTime == nil {
now := time.Now().UTC()
firstSeenTime = &now
if err := egigw.setFirstSeenTag(*igw, time.Now().UTC()); err != nil {
logging.Errorf(
"Unable to apply first seen tag Egress IGW: %s, with error: %s", *igw.EgressOnlyInternetGatewayId, err)
continue
}
}

if shouldIncludeEgressOnlyInternetGateway(igw, firstSeenTime, configObj) {
result = append(result, igw.EgressOnlyInternetGatewayId)
}
}

// checking the nukable permissions
egigw.VerifyNukablePermissions(result, func(id *string) error {
_, err := egigw.Client.DeleteEgressOnlyInternetGateway(&ec2.DeleteEgressOnlyInternetGatewayInput{
EgressOnlyInternetGatewayId: id,
DryRun: aws.Bool(true),
})
return err
})

return result, nil
}

func (egigw *EgressOnlyInternetGateway) nukeAll(ids []*string) error {
if len(ids) == 0 {
logging.Debugf("No Egress only internet gateway ID's to nuke in region %s", egigw.Region)
return nil
}

logging.Debugf("Deleting all Egress only internet gateway in region %s", egigw.Region)
var deletedList []*string

for _, id := range ids {
// NOTE : We can skip the error checking and return it here, since it is already being checked while displaying the identifiers with the Nukable field.
// Here, `err` refers to the error indicating whether the identifier is eligible for nuke or not (an error which we got from aws when tried to delete the resource with dryRun),
// and it is not a programming error. (edited)
if nukable, err := egigw.IsNukable(*id); !nukable {
logging.Debugf("[Skipping] %s nuke because %v", *id, err)
continue
}

_, err := egigw.Client.DeleteEgressOnlyInternetGateway(&ec2.DeleteEgressOnlyInternetGatewayInput{
EgressOnlyInternetGatewayId: id,
})

// Record status of this resource
e := report.Entry{
Identifier: aws.StringValue(id),
ResourceType: "Egress Only Internet Gateway",
Error: err,
}
report.Record(e)

if err != nil {
logging.Debugf("[Failed] %s", err)
} else {
deletedList = append(deletedList, id)
logging.Debugf("Deleted egress only internet gateway: %s", *id)
}
}

logging.Debugf("[OK] %d Egress only internet gateway(s) deleted in %s", len(deletedList), egigw.Region)

return nil

}
171 changes: 171 additions & 0 deletions aws/resources/ec2_egress_only_igw_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
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 mockedEgressOnlyIgw struct {
BaseAwsResource
ec2iface.EC2API
DescribeEgressOnlyInternetGatewaysOutput ec2.DescribeEgressOnlyInternetGatewaysOutput
DeleteEgressOnlyInternetGatewayOutput ec2.DeleteEgressOnlyInternetGatewayOutput
}

func (m mockedEgressOnlyIgw) DescribeEgressOnlyInternetGateways(_ *ec2.DescribeEgressOnlyInternetGatewaysInput) (*ec2.DescribeEgressOnlyInternetGatewaysOutput, error) {
return &m.DescribeEgressOnlyInternetGatewaysOutput, nil
}

func (m mockedEgressOnlyIgw) DeleteEgressOnlyInternetGateway(_ *ec2.DeleteEgressOnlyInternetGatewayInput) (*ec2.DeleteEgressOnlyInternetGatewayOutput, error) {
return &m.DeleteEgressOnlyInternetGatewayOutput, nil
}

func TestEgressOnlyInternetGateway_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"
)
object := EgressOnlyInternetGateway{
Client: mockedEgressOnlyIgw{
DescribeEgressOnlyInternetGatewaysOutput: ec2.DescribeEgressOnlyInternetGatewaysOutput{
EgressOnlyInternetGateways: []*ec2.EgressOnlyInternetGateway{
{
EgressOnlyInternetGatewayId: 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)),
},
},
},
{
EgressOnlyInternetGatewayId: 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 * time.Hour))),
},
},
},
},
},
},
}
object.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),
}},
expected: []string{gateway1},
},
"timeBeforeExclusionFilter": {
configObj: config.ResourceType{
ExcludeRule: config.FilterRule{
TimeBefore: aws.Time(now.Add(1)),
}},
expected: []string{gateway2},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
names, err := object.getAll(context.Background(), config.Config{
EgressOnlyInternetGateway: tc.configObj,
})
require.NoError(t, err)
require.Equal(t, tc.expected, aws.StringValueSlice(names))
})
}

}

func TestEc2EgressOnlyInternetGateway_NukeAll(t *testing.T) {
telemetry.InitTelemetry("cloud-nuke", "")
t.Parallel()

var (
gateway1 = "igw-0b44cfa6103932e1d001"
gateway2 = "igw-0b44cfa6103932e1d002"
)

igw := EgressOnlyInternetGateway{
BaseAwsResource: BaseAwsResource{
Nukables: map[string]error{
gateway1: nil,
},
},
Client: mockedEgressOnlyIgw{
DescribeEgressOnlyInternetGatewaysOutput: ec2.DescribeEgressOnlyInternetGatewaysOutput{
EgressOnlyInternetGateways: []*ec2.EgressOnlyInternetGateway{
{
EgressOnlyInternetGatewayId: aws.String(gateway1),
Attachments: []*ec2.InternetGatewayAttachment{
{
State: aws.String("testing-state"),
VpcId: aws.String("test-gateway-vpc"),
},
},
},
{
EgressOnlyInternetGatewayId: aws.String(gateway2),
Attachments: []*ec2.InternetGatewayAttachment{
{
State: aws.String("testing-state"),
VpcId: aws.String("test-gateway-vpc"),
},
},
},
},
},
DeleteEgressOnlyInternetGatewayOutput: ec2.DeleteEgressOnlyInternetGatewayOutput{},
},
}

err := igw.nukeAll([]*string{
aws.String(gateway1),
aws.String(gateway2),
})
require.NoError(t, err)
}
Loading

0 comments on commit 3c3d3fb

Please sign in to comment.