Skip to content

Commit

Permalink
Add config file support for ElasticIP, AutoScalingGroup, LaunchConfig…
Browse files Browse the repository at this point in the history
…uration, EC2 (#287)

* add ASG config file support

* add Launch Configuration config file support

* Add ElasticIP config file support

* refactor name lookup

* Add EC2 instance config file support

* fix shouldIncludeAllocationId

* remove extraneous files

* fix readme
  • Loading branch information
brandonstrohmeyer authored Mar 16, 2022
1 parent fbbd28f commit 7ab9690
Show file tree
Hide file tree
Showing 12 changed files with 564 additions and 54 deletions.
58 changes: 37 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,18 @@ The following resources support the Config file:
- KMS customer keys
- Resource type: `kmscustomerkeys`
- Config key: `KMSCustomerKeys`
- Auto Scaling Groups
- Resource type: `asg`
- Config key: `AutoScalingGroup`
- Launch Configurations
- Resource type: `lc`
- Config key: `LaunchConfiguration`
- Elastic IP Address
- Resource type: `eip`
- Config key: `ElasticIP`
- EC2 Instances
- Resource type: `ec2`
- Config key: `EC2`

#### Example

Expand Down Expand Up @@ -288,29 +300,33 @@ Be careful when nuking and append the `--dry-run` option if you're unsure. Even

To find out what we options are supported in the config file today, consult this table. Resource types at the top level of the file that are supported are listed here.

| resource type | names | names_regex | tags | tags_regex |
|---------------------|-------|-------------|------|------------|
| s3 | none | ✅ | none | none |
| iam | none | ✅ | none | none |
| ecsserv | none | ✅ | none | none |
| ecscluster | none | ✅ | none | none |
| secretsmanager | none | ✅ | none | none |
| nat-gateway | none | ✅ | none | none |
| accessanalyzer | none | ✅ | none | none |
| dynamodb | none | ✅ | none | none |
| ebs | none | ✅ | none | none |
| lambda | none | ✅ | none | none |
| elbv2 | none | ✅ | none | none |
| ecs | none | ✅ | none | none |
| elasticache | none | ✅ | none | none |
| vpc | none | ✅ | none | none |
| oidcprovider | none | ✅ | none | none |
| resource type | names | names_regex | tags | tags_regex |
|--------------------|-------|-------------|------|------------|
| s3 | none | ✅ | none | none |
| iam | none | ✅ | none | none |
| ecsserv | none | ✅ | none | none |
| ecscluster | none | ✅ | none | none |
| secretsmanager | none | ✅ | none | none |
| nat-gateway | none | ✅ | none | none |
| accessanalyzer | none | ✅ | none | none |
| dynamodb | none | ✅ | none | none |
| ebs | none | ✅ | none | none |
| lambda | none | ✅ | none | none |
| elbv2 | none | ✅ | none | none |
| ecs | none | ✅ | none | none |
| elasticache | none | ✅ | none | none |
| vpc | none | ✅ | none | none |
| oidcprovider | none | ✅ | none | none |
| cloudwatch-loggroup | none | ✅ | none | none |
| kmscustomerkeys | none | ✅ | none | none |
| acmpca | none | none | none | none |
| ec2 instance | none | none | none | none |
| iam role | none | none | none | none |
| ... (more to come) | none | none | none | none |
| asg | none | ✅ | none | none |
| lc | none | ✅ | none | none |
| eip | none | ✅ | none | none |
| ec2 | none | ✅ | none | none |
| acmpca | none | none | none | none |
| iam role | none | none | none | none |
| ... (more to come) | none | none | none | none |




Expand Down
21 changes: 19 additions & 2 deletions aws/asg.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import (
awsgo "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/gruntwork-io/cloud-nuke/logging"
"github.com/gruntwork-io/go-commons/errors"
)

// Returns a formatted string of ASG Names
func getAllAutoScalingGroups(session *session.Session, region string, excludeAfter time.Time) ([]*string, error) {
func getAllAutoScalingGroups(session *session.Session, region string, excludeAfter time.Time, configObj config.Config) ([]*string, error) {
svc := autoscaling.New(session)
result, err := svc.DescribeAutoScalingGroups(&autoscaling.DescribeAutoScalingGroupsInput{})
if err != nil {
Expand All @@ -20,14 +21,30 @@ func getAllAutoScalingGroups(session *session.Session, region string, excludeAft

var groupNames []*string
for _, group := range result.AutoScalingGroups {
if excludeAfter.After(*group.CreatedTime) {
if shouldIncludeAutoScalingGroup(group, excludeAfter, configObj) {
groupNames = append(groupNames, group.AutoScalingGroupName)
}
}

return groupNames, nil
}

func shouldIncludeAutoScalingGroup(group *autoscaling.Group, excludeAfter time.Time, configObj config.Config) bool {
if group == nil {
return false
}

if group.CreatedTime != nil && excludeAfter.Before(*group.CreatedTime) {
return false
}

return config.ShouldInclude(
awsgo.StringValue(group.AutoScalingGroupName),
configObj.AutoScalingGroup.IncludeRule.NamesRegExp,
configObj.AutoScalingGroup.ExcludeRule.NamesRegExp,
)
}

// Deletes all Auto Scaling Groups
func nukeAllAutoScalingGroups(session *session.Session, groupNames []*string) error {
svc := autoscaling.New(session)
Expand Down
83 changes: 80 additions & 3 deletions aws/asg_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package aws

import (
"regexp"
"testing"
"time"

awsgo "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/autoscaling"
"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/go-commons/errors"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -58,14 +61,14 @@ func TestListAutoScalingGroups(t *testing.T) {
defer nukeAllAutoScalingGroups(session, []*string{&uniqueTestID})
defer nukeAllEc2Instances(session, findEC2InstancesByNameTag(t, session, uniqueTestID))

groupNames, err := getAllAutoScalingGroups(session, region, time.Now().Add(1*time.Hour*-1))
groupNames, err := getAllAutoScalingGroups(session, region, time.Now().Add(1*time.Hour*-1), config.Config{})
if err != nil {
assert.Fail(t, "Unable to fetch list of Auto Scaling Groups")
}

assert.NotContains(t, awsgo.StringValueSlice(groupNames), uniqueTestID)

groupNames, err = getAllAutoScalingGroups(session, region, time.Now().Add(1*time.Hour))
groupNames, err = getAllAutoScalingGroups(session, region, time.Now().Add(1*time.Hour), config.Config{})
if err != nil {
assert.Fail(t, "Unable to fetch list of Auto Scaling Groups")
}
Expand Down Expand Up @@ -107,10 +110,84 @@ func TestNukeAutoScalingGroups(t *testing.T) {
assert.Fail(t, errors.WithStackTrace(err).Error())
}

groupNames, err := getAllAutoScalingGroups(session, region, time.Now().Add(1*time.Hour))
groupNames, err := getAllAutoScalingGroups(session, region, time.Now().Add(1*time.Hour), config.Config{})
if err != nil {
assert.Fail(t, "Unable to fetch list of Auto Scaling Groups")
}

assert.NotContains(t, awsgo.StringValueSlice(groupNames), uniqueTestID)
}

// Test config file filtering works as expected
func TestShouldIncludeAutoScalingGroup(t *testing.T) {
mockAutoScalingGroup := &autoscaling.Group{
AutoScalingGroupName: awsgo.String("cloud-nuke-test"),
CreatedTime: awsgo.Time(time.Now()),
}

mockExpression, err := regexp.Compile("^cloud-nuke-*")
if err != nil {
logging.Logger.Fatalf("There was an error compiling regex expression %v", err)
}

mockExcludeConfig := config.Config{
AutoScalingGroup: config.ResourceType{
ExcludeRule: config.FilterRule{
NamesRegExp: []config.Expression{
{
RE: *mockExpression,
},
},
},
},
}

mockIncludeConfig := config.Config{
AutoScalingGroup: config.ResourceType{
IncludeRule: config.FilterRule{
NamesRegExp: []config.Expression{
{
RE: *mockExpression,
},
},
},
},
}

cases := []struct {
Name string
AutoScalingGroup *autoscaling.Group
Config config.Config
ExcludeAfter time.Time
Expected bool
}{
{
Name: "ConfigExclude",
AutoScalingGroup: mockAutoScalingGroup,
Config: mockExcludeConfig,
ExcludeAfter: time.Now().Add(1 * time.Hour),
Expected: false,
},
{
Name: "ConfigInclude",
AutoScalingGroup: mockAutoScalingGroup,
Config: mockIncludeConfig,
ExcludeAfter: time.Now().Add(1 * time.Hour),
Expected: true,
},
{
Name: "NotOlderThan",
AutoScalingGroup: mockAutoScalingGroup,
Config: config.Config{},
ExcludeAfter: time.Now().Add(1 * time.Hour * -1),
Expected: false,
},
}

for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
result := shouldIncludeAutoScalingGroup(c.AutoScalingGroup, c.ExcludeAfter, c.Config)
assert.Equal(t, c.Expected, result)
})
}
}
8 changes: 4 additions & 4 deletions aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
// ASG Names
asGroups := ASGroups{}
if IsNukeable(asGroups.ResourceName(), resourceTypes) {
groupNames, err := getAllAutoScalingGroups(session, region, excludeAfter)
groupNames, err := getAllAutoScalingGroups(session, region, excludeAfter, configObj)
if err != nil {
return nil, errors.WithStackTrace(err)
}
Expand All @@ -271,7 +271,7 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
// Launch Configuration Names
configs := LaunchConfigs{}
if IsNukeable(configs.ResourceName(), resourceTypes) {
configNames, err := getAllLaunchConfigurations(session, region, excludeAfter)
configNames, err := getAllLaunchConfigurations(session, region, excludeAfter, configObj)
if err != nil {
return nil, errors.WithStackTrace(err)
}
Expand Down Expand Up @@ -401,7 +401,7 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
// EC2 Instances
ec2Instances := EC2Instances{}
if IsNukeable(ec2Instances.ResourceName(), resourceTypes) {
instanceIds, err := getAllEc2Instances(session, region, excludeAfter)
instanceIds, err := getAllEc2Instances(session, region, excludeAfter, configObj)
if err != nil {
return nil, errors.WithStackTrace(err)
}
Expand Down Expand Up @@ -429,7 +429,7 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
// EIP Addresses
eipAddresses := EIPAddresses{}
if IsNukeable(eipAddresses.ResourceName(), resourceTypes) {
allocationIds, err := getAllEIPAddresses(session, region, excludeAfter)
allocationIds, err := getAllEIPAddresses(session, region, excludeAfter, configObj)
if err != nil {
return nil, errors.WithStackTrace(err)
}
Expand Down
56 changes: 46 additions & 10 deletions aws/ec2.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import (
"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/cloud-nuke/logging"
"github.com/gruntwork-io/go-commons/errors"
)

// returns only instance Ids of unprotected ec2 instances
func filterOutProtectedInstances(svc *ec2.EC2, output *ec2.DescribeInstancesOutput, excludeAfter time.Time) ([]*string, error) {
func filterOutProtectedInstances(svc *ec2.EC2, output *ec2.DescribeInstancesOutput, excludeAfter time.Time, configObj config.Config) ([]*string, error) {
var filteredIds []*string
for _, reservation := range output.Reservations {
for _, instance := range reservation.Instances {
Expand All @@ -28,13 +29,8 @@ func filterOutProtectedInstances(svc *ec2.EC2, output *ec2.DescribeInstancesOutp
if err != nil {
return nil, errors.WithStackTrace(err)
}

protected := *attr.DisableApiTermination.Value
// Exclude protected EC2 instances
if !protected {
if excludeAfter.After(*instance.LaunchTime) {
filteredIds = append(filteredIds, &instanceID)
}
if shouldIncludeInstanceId(instance, excludeAfter, *attr.DisableApiTermination.Value, configObj) {
filteredIds = append(filteredIds, &instanceID)
}
}
}
Expand All @@ -43,7 +39,7 @@ func filterOutProtectedInstances(svc *ec2.EC2, output *ec2.DescribeInstancesOutp
}

// Returns a formatted string of EC2 instance ids
func getAllEc2Instances(session *session.Session, region string, excludeAfter time.Time) ([]*string, error) {
func getAllEc2Instances(session *session.Session, region string, excludeAfter time.Time, configObj config.Config) ([]*string, error) {
svc := ec2.New(session)

params := &ec2.DescribeInstancesInput{
Expand All @@ -63,14 +59,39 @@ func getAllEc2Instances(session *session.Session, region string, excludeAfter ti
return nil, errors.WithStackTrace(err)
}

instanceIds, err := filterOutProtectedInstances(svc, output, excludeAfter)
instanceIds, err := filterOutProtectedInstances(svc, output, excludeAfter, configObj)
if err != nil {
return nil, errors.WithStackTrace(err)
}

return instanceIds, nil
}

func shouldIncludeInstanceId(instance *ec2.Instance, excludeAfter time.Time, protected bool, configObj config.Config) bool {

if instance == nil {
return false
}

if excludeAfter.Before(*instance.LaunchTime) {
return false
}

if protected {
return false
}

// If Name is unset, GetEC2ResourceNameTagValue returns error and zero value string
// Ignore this error and pass empty string to config.ShouldInclude
instanceName, _ := GetEC2ResourceNameTagValue(instance.Tags)

return config.ShouldInclude(
instanceName,
configObj.EC2.IncludeRule.NamesRegExp,
configObj.EC2.ExcludeRule.NamesRegExp,
)
}

// Deletes all non protected EC2 instances
func nukeAllEc2Instances(session *session.Session, instanceIds []*string) error {
svc := ec2.New(session)
Expand Down Expand Up @@ -552,3 +573,18 @@ func NukeDefaultSecurityGroupRules(sgs []DefaultSecurityGroup) error {
logging.Logger.Info("Finished nuking default Security Groups in all regions")
return nil
}

// Given an map of tags, return the value of the Name tag
func GetEC2ResourceNameTagValue(tags []*ec2.Tag) (string, error) {
t := make(map[string]string)

for _, v := range tags {
t[awsgo.StringValue(v.Key)] = awsgo.StringValue(v.Value)
}

if name, ok := t["Name"]; ok {
return name, nil
}
return "", fmt.Errorf("Resource does not have Name tag")

}
Loading

0 comments on commit 7ab9690

Please sign in to comment.