Skip to content

Commit

Permalink
UI overhaul and per-resource error reporting (gruntwork-io#356)
Browse files Browse the repository at this point in the history
* Check in WIP

* Convert most Infof calls to Debugf

* Flip Info -> Debug

* Extract spinner to separate package for re-usability

* Update another Errorf call

* Convert remaining resources to table-based workflow

* Check in print tests

* Update README

* Handle pterm Render error

* Suppress S3 bucket deletion detection messages

* Suppress S3 warnings. Render error reason in table

* Implement progressbar option for responsive output

* Suppress more logging behind Debug calls

* Fix some progressbar bugs

* Implement pterm elements in UI refactor

* Fix issues preventing clean processing of all resources

* Handle render errors. Remove newlines from error msgs

* Handle spinnerErr

* Update report tests and fix uncovered bugs

* Update UI / Rendering tests

* Remove isActive check

* Update Cloudtrail resource to use record pattern

* Implement conditional generic error reporting
- Don't return immediately when a list error is encountered
- This should help prevent cloud-nuke from returning early when
it still has work to do

* fix typo nit

Co-authored-by: Andrew Ellison <[email protected]>
  • Loading branch information
zackproser and Andrew Ellison authored Dec 8, 2022
1 parent 559a80f commit 17ebebf
Show file tree
Hide file tree
Showing 55 changed files with 1,655 additions and 410 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ cloud-nuke aws --resource-type ec2 --dry-run
Dry run mode is only available within:
- `cloud-nuke aws`



### Using cloud-nuke as a library

You can import cloud-nuke into other projects and use it as a library for programmatically inspecting and counting resources.
Expand Down Expand Up @@ -501,6 +503,9 @@ To find out what we options are supported in the config file today, consult this


### Log level
By default, cloud-nuke sends most output to the `Debug` level logger, to enhance legibility, since the results of every deletion attempt will be displayed in the report that cloud-nuke prints after each run.

However, sometimes it's helpful to see all output, such as when you're debugging something.

You can set the log level by specifying the `--log-level` flag as per [logrus](https://github.com/sirupsen/logrus) log levels:

Expand Down
6 changes: 3 additions & 3 deletions aws/access_analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func shouldIncludeAccessAnalyzer(analyzer *accessanalyzer.AnalyzerSummary, exclu

func nukeAllAccessAnalyzers(session *session.Session, names []*string) error {
if len(names) == 0 {
logging.Logger.Infof("No IAM Access Analyzers to nuke in region %s", *session.Config.Region)
logging.Logger.Debugf("No IAM Access Analyzers to nuke in region %s", *session.Config.Region)
return nil
}

Expand All @@ -63,7 +63,7 @@ func nukeAllAccessAnalyzers(session *session.Session, names []*string) error {
}

// There is no bulk delete access analyzer API, so we delete the batch of Access Analyzers concurrently using go routines.
logging.Logger.Infof("Deleting all Access Analyzers in region %s", *session.Config.Region)
logging.Logger.Debugf("Deleting all Access Analyzers in region %s", *session.Config.Region)

svc := accessanalyzer.New(session)
wg := new(sync.WaitGroup)
Expand All @@ -80,7 +80,7 @@ func nukeAllAccessAnalyzers(session *session.Session, names []*string) error {
for _, errChan := range errChans {
if err := <-errChan; err != nil {
allErrs = multierror.Append(allErrs, err)
logging.Logger.Errorf("[Failed] %s", err)
logging.Logger.Debugf("[Failed] %s", err)
}
}
finalErr := allErrs.ErrorOrNil()
Expand Down
27 changes: 19 additions & 8 deletions aws/acmpca.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/aws/aws-sdk-go/service/acmpca"
"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"
"github.com/hashicorp/go-multierror"
)
Expand Down Expand Up @@ -64,12 +65,12 @@ func shouldIncludeACMPCA(ca *acmpca.CertificateAuthority, excludeAfter time.Time
// nukeAllACMPCA will delete all ACMPCA, which are given by a list of arns.
func nukeAllACMPCA(session *session.Session, arns []*string) error {
if len(arns) == 0 {
logging.Logger.Infof("No ACMPCA to nuke in region %s", *session.Config.Region)
logging.Logger.Debugf("No ACMPCA to nuke in region %s", *session.Config.Region)
return nil
}
svc := acmpca.New(session)

logging.Logger.Infof("Deleting all ACMPCA in region %s", *session.Config.Region)
logging.Logger.Debugf("Deleting all ACMPCA in region %s", *session.Config.Region)
// There is no bulk delete acmpca API, so we delete the batch of ARNs concurrently using go routines.
wg := new(sync.WaitGroup)
wg.Add(len(arns))
Expand All @@ -96,7 +97,7 @@ func nukeAllACMPCA(session *session.Session, arns []*string) error {
func deleteACMPCAASync(wg *sync.WaitGroup, errChan chan error, svc *acmpca.ACMPCA, arn *string, region string) {
defer wg.Done()

logging.Logger.Infof("Fetching details of CA to be deleted for ACMPCA %s in region %s", *arn, region)
logging.Logger.Debugf("Fetching details of CA to be deleted for ACMPCA %s in region %s", *arn, region)
details, detailsErr := svc.DescribeCertificateAuthority(&acmpca.DescribeCertificateAuthorityInput{CertificateAuthorityArn: arn})
if detailsErr != nil {
errChan <- detailsErr
Expand All @@ -119,27 +120,37 @@ func deleteACMPCAASync(wg *sync.WaitGroup, errChan chan error, svc *acmpca.ACMPC
statusSafe != acmpca.CertificateAuthorityStatusDeleted

if shouldUpdateStatus {
logging.Logger.Infof("Setting status to 'DISABLED' for ACMPCA %s in region %s", *arn, region)
logging.Logger.Debugf("Setting status to 'DISABLED' for ACMPCA %s in region %s", *arn, region)
if _, updateStatusErr := svc.UpdateCertificateAuthority(&acmpca.UpdateCertificateAuthorityInput{
CertificateAuthorityArn: arn,
Status: aws.String(acmpca.CertificateAuthorityStatusDisabled),
}); updateStatusErr != nil {
errChan <- updateStatusErr
return
}
logging.Logger.Infof("Did set status to 'DISABLED' for ACMPCA: %s in region %s", *arn, region)
logging.Logger.Debugf("Did set status to 'DISABLED' for ACMPCA: %s in region %s", *arn, region)
}

if _, deleteErr := svc.DeleteCertificateAuthority(&acmpca.DeleteCertificateAuthorityInput{
_, deleteErr := svc.DeleteCertificateAuthority(&acmpca.DeleteCertificateAuthorityInput{
CertificateAuthorityArn: arn,
// the range is 7 to 30 days.
// since cloud-nuke should not be used in production,
// we assume that the minimum (7 days) is fine.
PermanentDeletionTimeInDays: aws.Int64(7),
}); deleteErr != nil {
})

// Record status of this resource
e := report.Entry{
Identifier: aws.StringValue(arn),
ResourceType: "ACM Private CA (ACMPCA)",
Error: deleteErr,
}
report.Record(e)

if deleteErr != nil {
errChan <- deleteErr
return
}
logging.Logger.Infof("Deleted ACMPCA: %s successfully", *arn)
logging.Logger.Debugf("Deleted ACMPCA: %s successfully", *arn)
errChan <- nil
}
21 changes: 16 additions & 5 deletions aws/ami.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package aws
import (
"time"

"github.com/aws/aws-sdk-go/aws"
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/gruntwork-io/cloud-nuke/logging"
"github.com/gruntwork-io/cloud-nuke/report"
"github.com/gruntwork-io/go-commons/errors"
)

Expand Down Expand Up @@ -44,11 +46,11 @@ func nukeAllAMIs(session *session.Session, imageIds []*string) error {
svc := ec2.New(session)

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

logging.Logger.Infof("Deleting all AMIs in region %s", *session.Config.Region)
logging.Logger.Debugf("Deleting all AMIs in region %s", *session.Config.Region)

deletedCount := 0
for _, imageID := range imageIds {
Expand All @@ -57,14 +59,23 @@ func nukeAllAMIs(session *session.Session, imageIds []*string) error {
}

_, err := svc.DeregisterImage(params)

// Record status of this resource
e := report.Entry{
Identifier: aws.StringValue(imageID),
ResourceType: "Amazon Machine Image (AMI)",
Error: err,
}
report.Record(e)

if err != nil {
logging.Logger.Errorf("[Failed] %s", err)
logging.Logger.Debugf("[Failed] %s", err)
} else {
deletedCount++
logging.Logger.Infof("Deleted AMI: %s", *imageID)
logging.Logger.Debugf("Deleted AMI: %s", *imageID)
}
}

logging.Logger.Infof("[OK] %d AMI(s) terminated in %s", deletedCount, *session.Config.Region)
logging.Logger.Debugf("[OK] %d AMI(s) terminated in %s", deletedCount, *session.Config.Region)
return nil
}
19 changes: 14 additions & 5 deletions aws/apigateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go/service/apigateway"
"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"
"github.com/hashicorp/go-multierror"
)
Expand Down Expand Up @@ -54,7 +55,7 @@ func nukeAllAPIGateways(session *session.Session, identifiers []*string) error {
svc := apigateway.New(session)

if len(identifiers) == 0 {
logging.Logger.Infof("No API Gateways (v1) to nuke in region %s", region)
logging.Logger.Debugf("No API Gateways (v1) to nuke in region %s", region)
}

if len(identifiers) > 100 {
Expand All @@ -63,7 +64,7 @@ func nukeAllAPIGateways(session *session.Session, identifiers []*string) error {
}

// There is no bulk delete Api Gateway API, so we delete the batch of gateways concurrently using goroutines
logging.Logger.Infof("Deleting Api Gateways (v1) in region %s", region)
logging.Logger.Debugf("Deleting Api Gateways (v1) in region %s", region)
wg := new(sync.WaitGroup)
wg.Add(len(identifiers))
errChans := make([]chan error, len(identifiers))
Expand All @@ -77,7 +78,7 @@ func nukeAllAPIGateways(session *session.Session, identifiers []*string) error {
for _, errChan := range errChans {
if err := <-errChan; err != nil {
allErrs = multierror.Append(allErrs, err)
logging.Logger.Errorf("[Failed] %s", err)
logging.Logger.Debugf("[Failed] %s", err)
}
}
finalErr := allErrs.ErrorOrNil()
Expand All @@ -94,9 +95,17 @@ func deleteApiGatewayAsync(wg *sync.WaitGroup, errChan chan error, svc *apigatew
_, err := svc.DeleteRestApi(input)
errChan <- err

// Record status of this resource
e := report.Entry{
Identifier: *apigwID,
ResourceType: "APIGateway (v1)",
Error: err,
}
report.Record(e)

if err == nil {
logging.Logger.Infof("[OK] API Gateway (v1) %s deleted in %s", aws.StringValue(apigwID), region)
logging.Logger.Debugf("[OK] API Gateway (v1) %s deleted in %s", aws.StringValue(apigwID), region)
} else {
logging.Logger.Errorf("[Failed] Error deleting API Gateway (v1) %s in %s", aws.StringValue(apigwID), region)
logging.Logger.Debugf("[Failed] Error deleting API Gateway (v1) %s in %s", aws.StringValue(apigwID), region)
}
}
21 changes: 15 additions & 6 deletions aws/apigatewayv2.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go/service/apigatewayv2"
"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"
"github.com/hashicorp/go-multierror"
)
Expand Down Expand Up @@ -54,16 +55,16 @@ func nukeAllAPIGatewaysV2(session *session.Session, identifiers []*string) error
svc := apigatewayv2.New(session)

if len(identifiers) == 0 {
logging.Logger.Infof("No API Gateways (v2) to nuke in region %s", region)
logging.Logger.Debugf("No API Gateways (v2) to nuke in region %s", region)
}

if len(identifiers) > 100 {
logging.Logger.Errorf("Nuking too many API Gateways (v2) at once (100): halting to avoid hitting AWS API rate limiting")
logging.Logger.Debugf("Nuking too many API Gateways (v2) at once (100): halting to avoid hitting AWS API rate limiting")
return TooManyApiGatewayV2Err{}
}

// There is no bulk delete Api Gateway API, so we delete the batch of gateways concurrently using goroutines
logging.Logger.Infof("Deleting Api Gateways (v2) in region %s", region)
logging.Logger.Debugf("Deleting Api Gateways (v2) in region %s", region)
wg := new(sync.WaitGroup)
wg.Add(len(identifiers))
errChans := make([]chan error, len(identifiers))
Expand All @@ -77,7 +78,7 @@ func nukeAllAPIGatewaysV2(session *session.Session, identifiers []*string) error
for _, errChan := range errChans {
if err := <-errChan; err != nil {
allErrs = multierror.Append(allErrs, err)
logging.Logger.Errorf("[Failed] %s", err)
logging.Logger.Debugf("[Failed] %s", err)
}
}
finalErr := allErrs.ErrorOrNil()
Expand All @@ -94,9 +95,17 @@ func deleteApiGatewayAsyncV2(wg *sync.WaitGroup, errChan chan error, svc *apigat
_, err := svc.DeleteApi(input)
errChan <- err

// Record status of this resource
e := report.Entry{
Identifier: *apiId,
ResourceType: "APIGateway (v2)",
Error: err,
}
report.Record(e)

if err == nil {
logging.Logger.Infof("[OK] API Gateway (v2) %s deleted in %s", aws.StringValue(apiId), region)
logging.Logger.Debugf("[OK] API Gateway (v2) %s deleted in %s", aws.StringValue(apiId), region)
} else {
logging.Logger.Errorf("[Failed] Error deleting API Gateway (v2) %s in %s", aws.StringValue(apiId), region)
logging.Logger.Debugf("[Failed] Error deleting API Gateway (v2) %s in %s", aws.StringValue(apiId), region)
}
}
21 changes: 15 additions & 6 deletions aws/asg.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"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/report"
"github.com/gruntwork-io/go-commons/errors"
)

Expand Down Expand Up @@ -50,11 +51,11 @@ func nukeAllAutoScalingGroups(session *session.Session, groupNames []*string) er
svc := autoscaling.New(session)

if len(groupNames) == 0 {
logging.Logger.Infof("No Auto Scaling Groups to nuke in region %s", *session.Config.Region)
logging.Logger.Debugf("No Auto Scaling Groups to nuke in region %s", *session.Config.Region)
return nil
}

logging.Logger.Infof("Deleting all Auto Scaling Groups in region %s", *session.Config.Region)
logging.Logger.Debugf("Deleting all Auto Scaling Groups in region %s", *session.Config.Region)
var deletedGroupNames []*string

for _, groupName := range groupNames {
Expand All @@ -64,25 +65,33 @@ func nukeAllAutoScalingGroups(session *session.Session, groupNames []*string) er
}

_, err := svc.DeleteAutoScalingGroup(params)

// Record status of this resource
e := report.Entry{
Identifier: *groupName,
ResourceType: "Auto-Scaling Group",
Error: err,
}
report.Record(e)

if err != nil {
logging.Logger.Errorf("[Failed] %s", err)
logging.Logger.Debugf("[Failed] %s", err)
} else {
deletedGroupNames = append(deletedGroupNames, groupName)
logging.Logger.Infof("Deleted Auto Scaling Group: %s", *groupName)
logging.Logger.Debugf("Deleted Auto Scaling Group: %s", *groupName)
}
}

if len(deletedGroupNames) > 0 {
err := svc.WaitUntilGroupNotExists(&autoscaling.DescribeAutoScalingGroupsInput{
AutoScalingGroupNames: deletedGroupNames,
})

if err != nil {
logging.Logger.Errorf("[Failed] %s", err)
return errors.WithStackTrace(err)
}
}

logging.Logger.Infof("[OK] %d Auto Scaling Group(s) deleted in %s", len(deletedGroupNames), *session.Config.Region)
logging.Logger.Debugf("[OK] %d Auto Scaling Group(s) deleted in %s", len(deletedGroupNames), *session.Config.Region)
return nil
}
Loading

0 comments on commit 17ebebf

Please sign in to comment.