Skip to content

Commit

Permalink
Support resource EC2 Placement Groups (#758)
Browse files Browse the repository at this point in the history
Signed-off-by: Frank Lichtenheld <[email protected]>
  • Loading branch information
flichtenheld authored Aug 7, 2024
1 parent ef5d750 commit 6c07bd2
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Cloud-nuke suppports 🔎 inspecting and 🔥💀 deleting the following AWS res
| EC2 | Endpoint |
| EC2 | Security Group |
| EC2 | Network Interface |
| EC2 | Placement Group |
| Certificate Manager | ACM Private CA |
| Direct Connect | Transit Gateways |
| Elasticache | Clusters |
Expand Down Expand Up @@ -577,6 +578,7 @@ of the file that are supported are listed here.
| 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-placement-groups | EC2PlacementGroups | ✅ (Placement Group Name) | ✅ (First Seen Tag Time) | ✅ | ✅ |
| ec2-subnet | EC2Subnet | ✅ (Subnet Name) | ✅ (Creation Time) | ✅ | ❌ |
| ec2-endpoint | EC2Endpoint | ✅ (Endpoint Name) | ✅ (Creation Time) | ✅ | ✅ |
| ecr | ECRRepository | ✅ (Repository 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 @@ -75,6 +75,7 @@ func getRegisteredRegionalResources() []AwsResource {
&resources.EC2Instances{},
&resources.EC2DedicatedHosts{},
&resources.EC2KeyPairs{},
&resources.EC2PlacementGroups{},
&resources.TransitGateways{},
&resources.TransitGatewaysRouteTables{},
// Note: nuking transitgateway vpc attachement before nuking the vpc since vpc could be associated with it.
Expand Down
95 changes: 95 additions & 0 deletions aws/resources/ec2_placement_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package resources

import (
"context"
"time"

awsgo "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/util"
"github.com/gruntwork-io/gruntwork-cli/errors"
"github.com/hashicorp/go-multierror"
)

// getAll extracts the list of existing ec2 placement groups
func (p *EC2PlacementGroups) getAll(c context.Context, configObj config.Config) ([]*string, error) {
var names []*string
var firstSeenTime *time.Time

result, err := p.Client.DescribePlacementGroupsWithContext(p.Context, &ec2.DescribePlacementGroupsInput{})
if err != nil {
return nil, errors.WithStackTrace(err)
}
for _, placementGroup := range result.PlacementGroups {
firstSeenTime, err = util.GetOrCreateFirstSeen(c, p.Client, placementGroup.GroupId, util.ConvertEC2TagsToMap(placementGroup.Tags))
if err != nil {
logging.Error("Unable to retrieve tags")
return nil, errors.WithStackTrace(err)
}

if configObj.EC2PlacementGroups.ShouldInclude(config.ResourceValue{
Name: placementGroup.GroupName,
Time: firstSeenTime,
Tags: util.ConvertEC2TagsToMap(placementGroup.Tags),
}) {
names = append(names, placementGroup.GroupName)
}
}

// checking the nukable permissions
p.VerifyNukablePermissions(names, func(name *string) error {
_, err := p.Client.DeletePlacementGroupWithContext(p.Context, &ec2.DeletePlacementGroupInput{
GroupName: name,
DryRun: awsgo.Bool(true),
})
return err
})

return names, nil
}

// deleteKeyPair is a helper method that deletes the given ec2 key pair.
func (p *EC2PlacementGroups) deletePlacementGroup(placementGroupName *string) error {
params := &ec2.DeletePlacementGroupInput{
GroupName: placementGroupName,
}

_, err := p.Client.DeletePlacementGroupWithContext(p.Context, params)
if err != nil {
return errors.WithStackTrace(err)
}

return nil
}

// nukeAllEc2KeyPairs attempts to delete given ec2 key pair IDs.
func (p *EC2PlacementGroups) nukeAll(groupNames []*string) error {
if len(groupNames) == 0 {
logging.Infof("No EC2 placement groups to nuke in region %s", p.Region)
return nil
}

logging.Infof("Terminating all EC2 placement groups in region %s", p.Region)

deletedPlacementGroups := 0
var multiErr *multierror.Error
for _, groupName := range groupNames {
if nukable, reason := p.IsNukable(awsgo.StringValue(groupName)); !nukable {
logging.Debugf("[Skipping] %s nuke because %v", awsgo.StringValue(groupName), reason)
continue
}

if err := p.deletePlacementGroup(groupName); err != nil {
logging.Errorf("[Failed] %s", err)
multiErr = multierror.Append(multiErr, err)
} else {
deletedPlacementGroups++
logging.Infof("Deleted EC2 Placement Group: %s", *groupName)
}
}

logging.Infof("[OK] %d EC2 Placement Group(s) terminated", deletedPlacementGroups)
return multiErr.ErrorOrNil()
}
121 changes: 121 additions & 0 deletions aws/resources/ec2_placement_group_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package resources

import (
"context"
"regexp"
"testing"
"time"

awsgo "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"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/util"
"github.com/stretchr/testify/require"
)

type mockedEC2PlacementGroups struct {
ec2iface.EC2API
DescribePlacementGroupsOutput ec2.DescribePlacementGroupsOutput
DeletePlacementGroupOutput ec2.DeletePlacementGroupOutput
}

func (m mockedEC2PlacementGroups) DescribePlacementGroupsWithContext(_ awsgo.Context, _ *ec2.DescribePlacementGroupsInput, _ ...request.Option) (*ec2.DescribePlacementGroupsOutput, error) {
return &m.DescribePlacementGroupsOutput, nil
}

func (m mockedEC2PlacementGroups) DeletePlacementGroupWithContext(_ awsgo.Context, _ *ec2.DeletePlacementGroupInput, _ ...request.Option) (*ec2.DeletePlacementGroupOutput, error) {
return &m.DeletePlacementGroupOutput, nil
}

func TestEC2PlacementGroups_GetAll(t *testing.T) {

t.Parallel()

// Set excludeFirstSeenTag to false for testing
ctx := context.WithValue(context.Background(), util.ExcludeFirstSeenTagKey, false)

now := time.Now()
testId1 := "test-group-id1"
testName1 := "test-group1"
testId2 := "test-group-id2"
testName2 := "test-group2"
p := EC2PlacementGroups{
Client: mockedEC2PlacementGroups{
DescribePlacementGroupsOutput: ec2.DescribePlacementGroupsOutput{
PlacementGroups: []*ec2.PlacementGroup{
{
GroupName: awsgo.String(testName1),
GroupId: awsgo.String(testId1),
Tags: []*ec2.Tag{{
Key: awsgo.String(util.FirstSeenTagKey),
Value: awsgo.String(util.FormatTimestamp(now)),
}},
},
{
GroupName: awsgo.String(testName2),
GroupId: awsgo.String(testId2),
Tags: []*ec2.Tag{{
Key: awsgo.String(util.FirstSeenTagKey),
Value: awsgo.String(util.FormatTimestamp(now.Add(2 * time.Hour))),
}},
},
},
},
},
}

tests := map[string]struct {
ctx context.Context
configObj config.ResourceType
expected []string
}{
"emptyFilter": {
ctx: ctx,
configObj: config.ResourceType{},
expected: []string{testName1, testName2},
},
"nameExclusionFilter": {
ctx: ctx,
configObj: config.ResourceType{
ExcludeRule: config.FilterRule{
NamesRegExp: []config.Expression{{
RE: *regexp.MustCompile(testName1),
}}},
},
expected: []string{testName2},
},
"timeAfterExclusionFilter": {
ctx: ctx,
configObj: config.ResourceType{
ExcludeRule: config.FilterRule{
TimeAfter: awsgo.Time(now.Add(1 * time.Hour)),
}},
expected: []string{testName1},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
names, err := p.getAll(tc.ctx, config.Config{
EC2PlacementGroups: tc.configObj,
})
require.NoError(t, err)
require.Equal(t, tc.expected, awsgo.StringValueSlice(names))
})
}
}

func TestEC2PlacementGroups_NukeAll(t *testing.T) {

t.Parallel()

h := EC2PlacementGroups{
Client: mockedEC2PlacementGroups{
DeletePlacementGroupOutput: ec2.DeletePlacementGroupOutput{},
},
}

err := h.nukeAll([]*string{awsgo.String("test-group1"), awsgo.String("test-group2")})
require.NoError(t, err)
}
60 changes: 60 additions & 0 deletions aws/resources/ec2_placement_group_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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"
)

type EC2PlacementGroups struct {
BaseAwsResource
Client ec2iface.EC2API
Region string
PlacementGroupNames []string
}

func (k *EC2PlacementGroups) Init(session *session.Session) {
k.Client = ec2.New(session)
}

// ResourceName - the simple name of the aws resource
func (k *EC2PlacementGroups) ResourceName() string {
return "ec2-placement-groups"
}

// ResourceIdentifiers - IDs of the ec2 key pairs
func (k *EC2PlacementGroups) ResourceIdentifiers() []string {
return k.PlacementGroupNames
}

func (k *EC2PlacementGroups) MaxBatchSize() int {
// Tentative batch size to ensure AWS doesn't throttle
return 200
}

func (k *EC2PlacementGroups) GetAndSetResourceConfig(configObj config.Config) config.ResourceType {
return configObj.EC2PlacementGroups
}

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

k.PlacementGroupNames = awsgo.StringValueSlice(identifiers)
return k.PlacementGroupNames, nil
}

func (k *EC2PlacementGroups) Nuke(identifiers []string) error {
if err := k.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 @@ -56,6 +56,7 @@ type Config struct {
EC2IPAMResourceDiscovery ResourceType `yaml:"EC2IPAMResourceDiscovery"`
EC2IPAMScope ResourceType `yaml:"EC2IPAMScope"`
EC2Endpoint ResourceType `yaml:"EC2Endpoint"`
EC2PlacementGroups ResourceType `yaml:"EC2PlacementGroups"`
EC2Subnet EC2ResourceType `yaml:"EC2Subnet"`
EgressOnlyInternetGateway ResourceType `yaml:"EgressOnlyInternetGateway"`
ECRRepository ResourceType `yaml:"ECRRepository"`
Expand Down
1 change: 1 addition & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func emptyConfig() *Config {
EC2IPAMResourceDiscovery: ResourceType{FilterRule{}, FilterRule{}, "",false},
EC2IPAMScope: ResourceType{FilterRule{}, FilterRule{}, "",false},
EC2Endpoint: ResourceType{FilterRule{}, FilterRule{}, "",false},
EC2PlacementGroups: ResourceType{FilterRule{}, FilterRule{}, "",false},
EC2Subnet: EC2ResourceType{false, ResourceType{FilterRule{}, FilterRule{}, "",false}},
EgressOnlyInternetGateway: ResourceType{FilterRule{}, FilterRule{}, "",false},
ECRRepository: ResourceType{FilterRule{}, FilterRule{}, "",false},
Expand Down

0 comments on commit 6c07bd2

Please sign in to comment.