Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement egress only internet gateway nuke #657

Merged
merged 1 commit into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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