Skip to content

Commit

Permalink
Add codedeploy (#469)
Browse files Browse the repository at this point in the history
* add codedeploy application

* use batch apis, add missing config

* remove unused code

* fix exclude after check

* docs: update for codedeploy

* add tests

* refactor batching for readability

* add async nuke

* use uniqueId, instead of randomString

* fix test config

* fix merge
  • Loading branch information
robpickerill committed Jul 3, 2023
1 parent 8d9464b commit b0d7647
Show file tree
Hide file tree
Showing 7 changed files with 432 additions and 3 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ Cloud-nuke suppports 🔎 inspecting and 🔥💀 deleting the following AWS res
| Security Hub | Members |
| Security Hub | Administrators |
| AWS Certificate Manager | Certificates |
| CodeDeploy | Applications |

> **WARNING:** The RDS APIs also interact with neptune and document db resources. Running `cloud-nuke aws --resource-type rds` without a config file will remove any neptune and document db resources in the account.
Expand Down Expand Up @@ -469,9 +470,9 @@ The following resources support the Config file:
- AWS Certificate Manager
- Resource type: `acm`
- Config key: `ACM`



- CodeDeploy
- Resource type: `codedeploy-application`
- Config key: `Codedeploy`

Notes:
* no configuration options for KMS customer keys, since keys are created with auto-generated identifier
Expand Down
28 changes: 28 additions & 0 deletions aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,33 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
}
// End Security Hub

// CodeDeploy Applications
codeDeployApplications := CodeDeployApplications{}
if IsNukeable(codeDeployApplications.ResourceName(), resourceTypes) {
start := time.Now()
applications, err := getAllCodeDeployApplications(cloudNukeSession, excludeAfter, configObj)
if err != nil {
ge := report.GeneralError{
Error: err,
Description: "Unable to retrieve CodeDeploy applications",
ResourceType: codeDeployApplications.ResourceName(),
}
report.RecordError(ge)
}
telemetry.TrackEvent(commonTelemetry.EventContext{
EventName: "Done Listing CodeDeploy Applications",
}, map[string]interface{}{
"region": region,
"recordCount": len(applications),
"actionTime": time.Since(start).Seconds(),
})
if len(applications) > 0 {
codeDeployApplications.AppNames = applications
resourcesInRegion.Resources = append(resourcesInRegion.Resources, codeDeployApplications)
}
}
// End CodeDeploy Applications

// ACM
acm := ACM{}
if IsNukeable(acm.ResourceName(), resourceTypes) {
Expand Down Expand Up @@ -1972,6 +1999,7 @@ func ListResourceTypes() []string {
SecurityHub{}.ResourceName(),
CloudWatchAlarms{}.ResourceName(),
ACM{}.ResourceName(),
CodeDeployApplications{}.ResourceName(),
}
sort.Strings(resourceTypes)
return resourceTypes
Expand Down
165 changes: 165 additions & 0 deletions aws/codedeploy_application.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package aws

import (
"sync"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/codedeploy"
"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/telemetry"
"github.com/gruntwork-io/go-commons/errors"
commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
"github.com/hashicorp/go-multierror"
)

func getAllCodeDeployApplications(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]string, error) {
svc := codedeploy.New(session)

codeDeployApplicationsFilteredByName := []string{}

err := svc.ListApplicationsPages(&codedeploy.ListApplicationsInput{}, func(page *codedeploy.ListApplicationsOutput, lastPage bool) bool {
for _, application := range page.Applications {
// Check if the CodeDeploy Application should be excluded by name as that information is available to us here.
// CreationDate is not available in the ListApplications API call, so we can't filter by that here, but we do filter by it later.
// By filtering the name here, we can reduce the number of BatchGetApplication API calls we have to make.
if config.ShouldInclude(*application, configObj.CodeDeployApplications.IncludeRule.NamesRegExp, configObj.CodeDeployApplications.ExcludeRule.NamesRegExp) {
codeDeployApplicationsFilteredByName = append(codeDeployApplicationsFilteredByName, *application)
}
}
return !lastPage
})
if err != nil {
return nil, err
}

// Check if the CodeDeploy Application should be excluded by CreationDate and return.
// We have to do this after the ListApplicationsPages API call because CreationDate is not available in that call.
return batchDescribeAndFilterCodeDeployApplications(session, codeDeployApplicationsFilteredByName, excludeAfter)
}

// batchDescribeAndFilterCodeDeployApplications - Describe the CodeDeploy Applications and filter out the ones that should be excluded by CreationDate.
func batchDescribeAndFilterCodeDeployApplications(session *session.Session, identifiers []string, excludeAfter time.Time) ([]string, error) {
svc := codedeploy.New(session)

// BatchGetApplications can only take 100 identifiers at a time, so we have to break up the identifiers into chunks of 100.
batchSize := 100
applicationNames := []string{}

for {
// if there are no identifiers left, then break out of the loop
if len(identifiers) == 0 {
break
}

// if the batch size is larger than the number of identifiers left, then set the batch size to the number of identifiers left
if len(identifiers) < batchSize {
batchSize = len(identifiers)
}

// get the next batch of identifiers
batch := aws.StringSlice(identifiers[:batchSize])
// then using that batch of identifiers, get the applicationsinfo
resp, err := svc.BatchGetApplications(
&codedeploy.BatchGetApplicationsInput{ApplicationNames: batch},
)
if err != nil {
return nil, err
}

// for each applicationsinfo, check if it should be excluded by creation date
for j := range resp.ApplicationsInfo {
if shouldNukeByCreationTime(resp.ApplicationsInfo[j], excludeAfter) {
applicationNames = append(applicationNames, *resp.ApplicationsInfo[j].ApplicationName)
}
}

// reduce the identifiers by the batch size we just processed, note that the slice header is mutated here
identifiers = identifiers[batchSize:]
}

return applicationNames, nil
}

// shouldNukeByCreationTime - Check if the CodeDeploy Application should be excluded by CreationDate.
func shouldNukeByCreationTime(applicationInfo *codedeploy.ApplicationInfo, excludeAfter time.Time) bool {
// If the CreationDate is nil, then we can't filter by it, so we return false as a precaution.
if applicationInfo == nil || applicationInfo.CreateTime == nil {
return false
}

// If the excludeAfter date is before the CreationDate, then we should not nuke the resource.
if excludeAfter.Before(*applicationInfo.CreateTime) {
return false
}

// Otherwise, the resource can be safely nuked.
return true
}

func nukeAllCodeDeployApplications(session *session.Session, identifiers []string) error {
svc := codedeploy.New(session)

if len(identifiers) == 0 {
logging.Logger.Debugf("No CodeDeploy Applications to nuke in region %s", *session.Config.Region)
return nil
}

logging.Logger.Infof("Deleting CodeDeploy Applications in region %s", *session.Config.Region)

var wg sync.WaitGroup
errChan := make(chan error, len(identifiers))

for _, identifier := range identifiers {
wg.Add(1)
go nukeCodeDeployApplicationAsync(svc, &wg, errChan, identifier)
}

wg.Wait()
close(errChan)

var allErrors *multierror.Error
for err := range errChan {
allErrors = multierror.Append(allErrors, err)

logging.Logger.Errorf("[Failed] Error deleting CodeDeploy Application: %s", err)
telemetry.TrackEvent(commonTelemetry.EventContext{
EventName: "Error Nuking CodeDeploy Application",
}, map[string]interface{}{
"region": *session.Config.Region,
})
}

finalErr := allErrors.ErrorOrNil()
if finalErr != nil {
return errors.WithStackTrace(finalErr)
}

return nil
}

func nukeCodeDeployApplicationAsync(svc *codedeploy.CodeDeploy, wg *sync.WaitGroup, errChan chan<- error, identifier string) {
defer wg.Done()

_, err := svc.DeleteApplication(&codedeploy.DeleteApplicationInput{ApplicationName: &identifier})
if err != nil {
errChan <- err
}

// record the status of the nuke attempt
e := report.Entry{
Identifier: identifier,
ResourceType: "CodeDeploy Application",
Error: err,
}
report.Record(e)

if err == nil {
logging.Logger.Debugf("[OK] Deleted CodeDeploy Application: %s", identifier)
} else {
logging.Logger.Debugf("[Failed] Error deleting CodeDeploy Application %s: %s", identifier, err)
}
}
Loading

0 comments on commit b0d7647

Please sign in to comment.