Skip to content

Commit

Permalink
implemented ec2-subnet nuke
Browse files Browse the repository at this point in the history
  • Loading branch information
james03160927 committed Mar 29, 2024
1 parent ca7dd2b commit 5f96bd5
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 1 deletion.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,10 +545,11 @@ of the file that are supported are listed here.
| ec2-dedicated-hosts | EC2DedicatedHosts | ✅ (EC2 Name Tag) | ✅ (Allocation Time) | ❌ | ❌ |
| ec2-dhcp-option | EC2DhcpOption | ❌ | ❌ | ❌ | ❌ |
| ec2-keypairs | EC2KeyPairs | ✅ (Key Pair Name) | ✅ (Creation Time) | ✅ | ❌ |
| ec2-ipam | EC2IPAM | ✅ (IPAM name) | ✅ (Creation Time) | ✅ | ❌ |
| ec2-ipam | EC2IPAM | ✅ (IPAM name) | ✅ (Creation Time) | ✅ | ❌ |
| ec2-ipam-pool | EC2IPAMPool | ✅ (IPAM Pool name) | ✅ (Creation Time) | ✅ | ❌ |
| ec2-ipam-resource-discovery | EC2IPAMResourceDiscovery | ✅ (IPAM Discovery Name) | ✅ (Creation Time) | ✅ | ❌ |
| ec2-ipam-scope | EC2IPAMScope | ✅ (IPAM Scope Name) | ✅ (Creation Time) | ✅ | ❌ |
| ec2-subnet | EC2Subnet | ✅ (Subnet Name) | ✅ (Creation Time) | ✅ | ❌ |
| ecr | ECRRepository | ✅ (Repository Name) | ✅ (Creation Time) | ❌ | ❌ |
| ecscluster | ECSCluster | ✅ (Cluster Name) | ❌ | ❌ | ❌ |
| ecsserv | ECSService | ✅ (Service Name) | ✅ (Creation 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 @@ -124,6 +124,7 @@ func getRegisteredRegionalResources() []AwsResource {
&resources.EC2IPAMPool{},
&resources.EC2IPAMByoasn{},
&resources.EC2IPAMCustomAllocation{},
&resources.EC2Subnet{},
&resources.Route53HostedZone{},
&resources.Route53CidrCollection{},
&resources.Route53TrafficPolicy{},
Expand Down
156 changes: 156 additions & 0 deletions aws/resources/ec2_subnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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 (ec2subnet *EC2Subnet) setFirstSeenTag(sb ec2.Subnet, value time.Time) error {
_, err := ec2subnet.Client.CreateTags(&ec2.CreateTagsInput{
Resources: []*string{sb.SubnetId},
Tags: []*ec2.Tag{
{
Key: aws.String(util.FirstSeenTagKey),
Value: aws.String(util.FormatTimestamp(value)),
},
},
})
if err != nil {
return errors.WithStackTrace(err)
}

return nil
}

func (ec2subnet *EC2Subnet) getFirstSeenTag(sb ec2.Subnet) (*time.Time, error) {
tags := sb.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 shouldIncludeEc2Subnet(subnet *ec2.Subnet, firstSeenTime *time.Time, configObj config.Config) bool {
var subnetName string
tagMap := util.ConvertEC2TagsToMap(subnet.Tags)
if name, ok := tagMap["Name"]; ok {
subnetName = name
}

return configObj.EC2Subnet.ShouldInclude(config.ResourceValue{
Name: &subnetName,
Time: firstSeenTime,
Tags: tagMap,
})
}

// Returns a formatted string of EC2 subnets
func (ec2subnet *EC2Subnet) getAll(_ context.Context, configObj config.Config) ([]*string, error) {
result := []*string{}
err := ec2subnet.Client.DescribeSubnetsPages(&ec2.DescribeSubnetsInput{}, func(pages *ec2.DescribeSubnetsOutput, lastPage bool) bool {
for _, subnet := range pages.Subnets {

// check first seen tag
firstSeenTime, err := ec2subnet.getFirstSeenTag(*subnet)
if err != nil {
logging.Errorf(
"Unable to retrieve tags for Subnet: %s, with error: %s", *subnet.SubnetId, err)
continue
}

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

if shouldIncludeEc2Subnet(subnet, firstSeenTime, configObj) {
result = append(result, subnet.SubnetId)
}
}
return !lastPage
})

if err != nil {
return nil, errors.WithStackTrace(err)
}

// check the resources are nukable
ec2subnet.VerifyNukablePermissions(result, func(id *string) error {
params := &ec2.DeleteSubnetInput{
SubnetId: id,
DryRun: aws.Bool(true), // dry run set as true , checks permission without actualy making the request
}
_, err := ec2subnet.Client.DeleteSubnet(params)
return err
})

return result, nil
}

// Deletes all Subnets
func (ec2subnet *EC2Subnet) nukeAll(ids []*string) error {
if len(ids) == 0 {
logging.Debugf("No Subnets to nuke in region %s", ec2subnet.Region)
return nil
}

logging.Debugf("Deleting all Subnets in region %s", ec2subnet.Region)
var deletedAddresses []*string

for _, id := range ids {
// check the id has the permission to nuke, if not. continue the execution
if nukable, err := ec2subnet.IsNukable(*id); !nukable {
// not adding the report on final result hence not adding a record entry here
// NOTE: We can skip the error checking and return it here, since it is already being checked while
// displaying the identifiers. Here, `err` refers to the error indicating whether the identifier is eligible for nuke or not,
// and it is not a programming error.
logging.Debugf("[Skipping] %s nuke because %v", *id, err)
continue
}

_, err := ec2subnet.Client.DeleteSubnet(&ec2.DeleteSubnetInput{
SubnetId: id,
})

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

if err != nil {
logging.Debugf("[Failed] %s", err)
} else {
deletedAddresses = append(deletedAddresses, id)
logging.Debugf("Deleted Subnet: %s", *id)
}
}

logging.Debugf("[OK] %d EC2 Subnet(s) deleted in %s", len(deletedAddresses), ec2subnet.Region)

return nil
}
127 changes: 127 additions & 0 deletions aws/resources/ec2_subnet_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
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 mockedEC2Subnets struct {
ec2iface.EC2API
DescribeSubnetsOutput ec2.DescribeSubnetsOutput
DeleteSubnetOutput ec2.DeleteSubnetOutput
}

func (m mockedEC2Subnets) DescribeSubnetsPages(_ *ec2.DescribeSubnetsInput, callback func(pages *ec2.DescribeSubnetsOutput, lastPage bool) bool) error {
callback(&m.DescribeSubnetsOutput, true)
return nil
}
func (m mockedEC2Subnets) DeleteSubnet(_ *ec2.DeleteSubnetInput) (*ec2.DeleteSubnetOutput, error) {
return &m.DeleteSubnetOutput, nil
}

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

var (
now = time.Now()
subnet1 = "subnet-0631b58700ba3db41"
testName1 = "cloud-nuke-subnet-001"
subnet2 = "subnet-0631b58700ba3db42"
testName2 = "cloud-nuke-subnet-002"
)

ec2subnet := EC2Subnet{
Client: mockedEC2Subnets{
DescribeSubnetsOutput: ec2.DescribeSubnetsOutput{
Subnets: []*ec2.Subnet{
{
SubnetId: aws.String(subnet1),
Tags: []*ec2.Tag{
{
Key: aws.String("Name"),
Value: aws.String(testName1),
}, {
Key: aws.String(util.FirstSeenTagKey),
Value: aws.String(util.FormatTimestamp(now)),
},
},
},
{
SubnetId: aws.String(subnet2),
Tags: []*ec2.Tag{
{
Key: aws.String("Name"),
Value: aws.String(testName2),
}, {
Key: aws.String(util.FirstSeenTagKey),
Value: aws.String(util.FormatTimestamp(now.Add(1))),
},
},
},
},
},
},
}
ec2subnet.BaseAwsResource.Init(nil)

tests := map[string]struct {
configObj config.ResourceType
expected []string
}{
"emptyFilter": {
configObj: config.ResourceType{},
expected: []string{subnet1, subnet2},
},
"nameExclusionFilter": {
configObj: config.ResourceType{
ExcludeRule: config.FilterRule{
NamesRegExp: []config.Expression{{
RE: *regexp.MustCompile(testName1),
}}},
},
expected: []string{subnet2},
},
"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 := ec2subnet.getAll(context.Background(), config.Config{
EC2Subnet: tc.configObj,
})
require.NoError(t, err)
require.Equal(t, tc.expected, aws.StringValueSlice(names))
})
}

}

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

tgw := EC2Subnet{
Client: mockedEC2Subnets{
DeleteSubnetOutput: ec2.DeleteSubnetOutput{},
},
}

err := tgw.nukeAll([]*string{aws.String("test-gateway")})
require.NoError(t, err)
}
59 changes: 59 additions & 0 deletions aws/resources/ec2_subnet_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package resources

import (
"context"

awsgo "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"
)

// Ec2Subnet- represents all Subnets
type EC2Subnet struct {
BaseAwsResource
Client ec2iface.EC2API
Region string
Subnets []string
}

func (es *EC2Subnet) Init(session *session.Session) {
es.BaseAwsResource.Init(session)
es.Client = ec2.New(session)
}

// ResourceName - the simple name of the aws resource
func (es *EC2Subnet) ResourceName() string {
return "ec2-subnet"
}

func (es *EC2Subnet) MaxBatchSize() int {
// Tentative batch size to ensure AWS doesn't throttle
return 49
}

// ResourceIdentifiers - The ids of the subnets
func (es *EC2Subnet) ResourceIdentifiers() []string {
return es.Subnets
}

func (es *EC2Subnet) GetAndSetIdentifiers(c context.Context, configObj config.Config) ([]string, error) {
identifiers, err := es.getAll(c, configObj)
if err != nil {
return nil, err
}

es.Subnets = awsgo.StringValueSlice(identifiers)
return es.Subnets, nil
}

// Nuke - nuke 'em all!!!
func (es *EC2Subnet) Nuke(identifiers []string) error {
if err := es.nukeAll(awsgo.StringSlice(identifiers)); err != nil {
return errors.WithStackTrace(err)
}

return nil
}
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type Config struct {
EC2IPAMPool ResourceType `yaml:"EC2IPAMPool"`
EC2IPAMResourceDiscovery ResourceType `yaml:"EC2IPAMResourceDiscovery"`
EC2IPAMScope ResourceType `yaml:"EC2IPAMScope"`
EC2Subnet ResourceType `yaml:"EC2Subnet"`
ECRRepository ResourceType `yaml:"ECRRepository"`
ECSCluster ResourceType `yaml:"ECSCluster"`
ECSService ResourceType `yaml:"ECSService"`
Expand Down
1 change: 1 addition & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func emptyConfig() *Config {
ResourceType{FilterRule{}, FilterRule{}, ""},
ResourceType{FilterRule{}, FilterRule{}, ""},
ResourceType{FilterRule{}, FilterRule{}, ""},
ResourceType{FilterRule{}, FilterRule{}, ""},
KMSCustomerKeyResourceType{false, ResourceType{FilterRule{}, FilterRule{}, ""}},
ResourceType{FilterRule{}, FilterRule{}, ""},
ResourceType{FilterRule{}, FilterRule{}, ""},
Expand Down

0 comments on commit 5f96bd5

Please sign in to comment.