From e9f66f64656878fd806f48ebd85a1209ba052390 Mon Sep 17 00:00:00 2001 From: James Kwon <96548424+hongil0316@users.noreply.github.com> Date: Tue, 27 Feb 2024 19:44:49 -0500 Subject: [PATCH] elastic bean stalk application implementation (#644) * elastic bean stalk application implementation * elastic bean stalk application implementation --- README.md | 2 + aws/resource_registry.go | 1 + aws/resources/elastic_beanstalk.go | 71 +++++++++++++++++ aws/resources/elastic_beanstalk_test.go | 99 ++++++++++++++++++++++++ aws/resources/elastic_beanstalk_types.go | 58 ++++++++++++++ config/config.go | 1 + config/config_test.go | 1 + 7 files changed, 233 insertions(+) create mode 100644 aws/resources/elastic_beanstalk.go create mode 100644 aws/resources/elastic_beanstalk_test.go create mode 100644 aws/resources/elastic_beanstalk_types.go diff --git a/README.md b/README.md index d1bc4560..522ab23f 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Cloud-nuke suppports 🔎 inspecting and 🔥💀 deleting the following AWS res | Elasticache | Clusters | | Elasticache | Parameter Groups | | Elasticache | Subnet Groups | +| Elastic Beanstalk | Applications | | ECS | Services | | ECS | Clusters | | EKS | Clusters | @@ -509,6 +510,7 @@ of the file that are supported are listed here. | rds-subnet-group | DBSubnetGroups | ✅ (DB Subnet Group Name) | ❌ | ❌ | | dynamodb | DynamoDB | ✅ (Table Name) | ✅ (Creation Time) | ❌ | | ebs | EBSVolume | ✅ (Volume Name) | ✅ (Creation Time) | ✅ | +| elastic-beanstalk | ElasticBeanstalk | ✅ (Application Name) | ✅ (Creation Time) | ❌ | | ec2 | EC2 | ✅ (Instance Name) | ✅ (Launch Time) | ✅ | | ec2-dedicated-hosts | EC2DedicatedHosts | ✅ (EC2 Name Tag) | ✅ (Allocation Time) | ❌ | | ec2-dhcp-option | EC2DhcpOption | ❌ | ❌ | ❌ | diff --git a/aws/resource_registry.go b/aws/resource_registry.go index 89ae28ca..d909c691 100644 --- a/aws/resource_registry.go +++ b/aws/resource_registry.go @@ -64,6 +64,7 @@ func getRegisteredRegionalResources() []AwsResource { &resources.ConfigServiceRule{}, &resources.DynamoDB{}, &resources.EBSVolumes{}, + &resources.EBApplications{}, &resources.EC2Instances{}, &resources.EC2DedicatedHosts{}, &resources.EC2KeyPairs{}, diff --git a/aws/resources/elastic_beanstalk.go b/aws/resources/elastic_beanstalk.go new file mode 100644 index 00000000..f54af16f --- /dev/null +++ b/aws/resources/elastic_beanstalk.go @@ -0,0 +1,71 @@ +package resources + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elasticbeanstalk" + "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/go-commons/errors" +) + +func shouldIncludeEBApplication(app *elasticbeanstalk.ApplicationDescription, configObj config.Config) bool { + return configObj.ElasticBeanstalk.ShouldInclude(config.ResourceValue{ + Name: app.ApplicationName, + Time: app.DateCreated, + }) +} + +// Returns a formatted string of EB application ids +func (eb *EBApplications) getAll(c context.Context, configObj config.Config) ([]*string, error) { + output, err := eb.Client.DescribeApplications(&elasticbeanstalk.DescribeApplicationsInput{}) + if err != nil { + return nil, errors.WithStackTrace(err) + } + var appIds []*string + for _, app := range output.Applications { + if shouldIncludeEBApplication(app, configObj) { + appIds = append(appIds, app.ApplicationName) + } + } + return appIds, nil +} + +// Deletes all EB Applications +func (eb *EBApplications) nukeAll(appIds []*string) error { + if len(appIds) == 0 { + logging.Debugf("No Elastic Beanstalk to nuke in region %s", eb.Region) + return nil + } + + logging.Debugf("Deleting all Elastic Beanstalk applications in region %s", eb.Region) + var deletedApps []*string + + for _, id := range appIds { + _, err := eb.Client.DeleteApplication(&elasticbeanstalk.DeleteApplicationInput{ + ApplicationName: id, + TerminateEnvByForce: aws.Bool(true), + }) + // Record status of this resource + e := report.Entry{ + Identifier: aws.StringValue(id), + ResourceType: "Elastic Beanstalk Application", + Error: err, + } + report.Record(e) + + if err != nil { + logging.Debugf("[Failed] %s", err) + continue + } + + // get the deleted ids + deletedApps = append(deletedApps, id) + logging.Debugf("Deleted Elastic Beanstalk application: %s", *id) + + } + logging.Debugf("[OK] %d Elastic Beanstalk application(s) deleted in %s", len(deletedApps), eb.Region) + return nil +} diff --git a/aws/resources/elastic_beanstalk_test.go b/aws/resources/elastic_beanstalk_test.go new file mode 100644 index 00000000..24d7f56e --- /dev/null +++ b/aws/resources/elastic_beanstalk_test.go @@ -0,0 +1,99 @@ +package resources + +import ( + "context" + "regexp" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elasticbeanstalk" + "github.com/aws/aws-sdk-go/service/elasticbeanstalk/elasticbeanstalkiface" + "github.com/gruntwork-io/cloud-nuke/config" + "github.com/stretchr/testify/require" +) + +type mockedEBApplication struct { + elasticbeanstalkiface.ElasticBeanstalkAPI + DescribeApplicationsOutput elasticbeanstalk.DescribeApplicationsOutput +} + +func (mock *mockedEBApplication) DescribeApplications(req *elasticbeanstalk.DescribeApplicationsInput) (*elasticbeanstalk.DescribeApplicationsOutput, error) { + return &mock.DescribeApplicationsOutput, nil +} + +func (mock *mockedEBApplication) DeleteApplication(*elasticbeanstalk.DeleteApplicationInput) (*elasticbeanstalk.DeleteApplicationOutput, error) { + return nil, nil +} + +func TestEBApplication_GetAll(t *testing.T) { + t.Parallel() + + app1 := "demo-app-golang-backend" + app2 := "demo-app-golang-frontend" + + now := time.Now() + eb := EBApplications{ + Client: &mockedEBApplication{ + DescribeApplicationsOutput: elasticbeanstalk.DescribeApplicationsOutput{ + Applications: []*elasticbeanstalk.ApplicationDescription{ + { + ApplicationArn: aws.String("app-arn-01"), + ApplicationName: &app1, + DateCreated: aws.Time(now), + }, + { + ApplicationArn: aws.String("app-arn-02"), + ApplicationName: &app2, + DateCreated: aws.Time(now.Add(1)), + }, + }, + }, + }, + } + + tests := map[string]struct { + configObj config.ResourceType + expected []string + }{ + "emptyFilter": { + configObj: config.ResourceType{}, + expected: []string{ + app1, app2, + }, + }, + "nameExclusionFilter": { + configObj: config.ResourceType{ + ExcludeRule: config.FilterRule{ + NamesRegExp: []config.Expression{{ + RE: *regexp.MustCompile(app1), + }}}, + }, + expected: []string{app2}, + }, + "timeAfterExclusionFilter": { + configObj: config.ResourceType{ + ExcludeRule: config.FilterRule{ + TimeAfter: aws.Time(now), + }}, + expected: []string{app1}, + }, + "timeBeforeExclusionFilter": { + configObj: config.ResourceType{ + ExcludeRule: config.FilterRule{ + TimeBefore: aws.Time(now.Add(1)), + }}, + expected: []string{app2}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + names, err := eb.getAll(context.Background(), config.Config{ + ElasticBeanstalk: tc.configObj, + }) + require.NoError(t, err) + require.Equal(t, tc.expected, aws.StringValueSlice(names)) + }) + } +} diff --git a/aws/resources/elastic_beanstalk_types.go b/aws/resources/elastic_beanstalk_types.go new file mode 100644 index 00000000..5a1b9224 --- /dev/null +++ b/aws/resources/elastic_beanstalk_types.go @@ -0,0 +1,58 @@ +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/elasticbeanstalk" + "github.com/aws/aws-sdk-go/service/elasticbeanstalk/elasticbeanstalkiface" + "github.com/gruntwork-io/cloud-nuke/config" + "github.com/gruntwork-io/go-commons/errors" +) + +// EBApplications - represents all elastic beanstalk applications +type EBApplications struct { + BaseAwsResource + Client elasticbeanstalkiface.ElasticBeanstalkAPI + Region string + appIds []string +} + +func (eb *EBApplications) Init(session *session.Session) { + eb.Client = elasticbeanstalk.New(session) +} + +// ResourceName - the simple name of the aws resource +func (eb *EBApplications) ResourceName() string { + return "elastic-beanstalk" +} + +// ResourceIdentifiers - The application ids of the elastic beanstalk +func (eb *EBApplications) ResourceIdentifiers() []string { + return eb.appIds +} + +func (eb *EBApplications) MaxBatchSize() int { + // Tentative batch size to ensure AWS doesn't throttle + return 49 +} + +func (eb *EBApplications) GetAndSetIdentifiers(c context.Context, configObj config.Config) ([]string, error) { + identifiers, err := eb.getAll(c, configObj) + if err != nil { + return nil, err + } + + eb.appIds = awsgo.StringValueSlice(identifiers) + return eb.appIds, nil +} + +// Nuke - nuke 'em all!!! +func (eb *EBApplications) Nuke(identifiers []string) error { + if err := eb.nukeAll(awsgo.StringSlice(identifiers)); err != nil { + return errors.WithStackTrace(err) + } + + return nil +} diff --git a/config/config.go b/config/config.go index 99def082..9ec5e65c 100644 --- a/config/config.go +++ b/config/config.go @@ -35,6 +35,7 @@ type Config struct { DBSubnetGroups ResourceType `yaml:"DBSubnetGroups"` DynamoDB ResourceType `yaml:"DynamoDB"` EBSVolume ResourceType `yaml:"EBSVolume"` + ElasticBeanstalk ResourceType `yaml:"ElasticBeanstalk"` EC2 ResourceType `yaml:"EC2"` EC2DedicatedHosts ResourceType `yaml:"EC2DedicatedHosts"` EC2DHCPOption ResourceType `yaml:"EC2DhcpOption"` diff --git a/config/config_test.go b/config/config_test.go index 3aeb81f3..a33c7c19 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -58,6 +58,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{}},