Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aws-nuke-v0.0.1 #4

Merged
merged 31 commits into from
Jan 25, 2018
Merged

aws-nuke-v0.0.1 #4

merged 31 commits into from
Jan 25, 2018

Conversation

tonerdo
Copy link
Contributor

@tonerdo tonerdo commented Jan 22, 2018

This PR contains a reimplementation of the original aws-nuke bash shell script in golang.

Changes Include:

  • Complete rewrite of cli utility in golang
  • Support for nuking only EC2 instances
  • Unit tests

Copy link
Member

@brikis98 brikis98 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woohoo! First PR :)

Overall: the code looks great. The structure is perfect and I'm very happy to see solid docs & tests.

I went through and left lots of detailed comments :)

README.md Outdated
# aws-nuke

This repo contains a CLI tool to delete all AWS resources in an account. The currently supported functionality includes:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably a good idea to add two more things to this intro section:

  1. Motivation: explain why aws-nuke exists and you might want to use it. E.g., you have an account you use for testing and need to clean up left over resources so AWS doesn't charge you for them.

  2. Warning: big, bold text explaining that this tool is destructive, irreversibly deletes things in your account, and that you must be very careful with its use.

README.md Outdated

1. Download the latest binary for your OS on the [releases page](https://github.com/gruntwork-io/aws-nuke/releases).
2. Move the binary to a folder on your `PATH`. E.g.: `mv gruntwork_darwin_amd64 /usr/local/bin/gruntwork`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update name of the binary here.


```shell
go test -v ./...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add an MIT license to this repo? See the license section in fetch for an example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

aws/aws.go Outdated
"us-east-1", "us-east-2", "us-west-1", "us-west-2", "ca-central-1",
"eu-west-1", "eu-central-1", "eu-west-2", "ap-southeast-1", "ap-southeast-2",
"ap-northeast-2", "ap-northeast-1", "ap-south-1", "sa-east-1",
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can get the list of regions programmatically from this API: https://docs.aws.amazon.com/sdk-for-go/api/aws/endpoints/

aws/aws.go Outdated

var resources []string
for _, region := range regions {
session, _ := session.NewSession(&awsgo.Config{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing the second parameter is an error object. If so, do not ignore it. Although it's verbose, the idiomatic code style in Go is for every function (including this GetAllResources function) to return a value (e.g., []string and an error):

func GetAllResources() ([]string, error) {
  // ...
  session, err := session.NewSession(&awsgo.Config{ ... })
  if err != nil {
    return nil, errors.WithStackTrace(err)
  }
}

aws/ec2_test.go Outdated
assert.Fail(t, "Unable to fetch list of EC2 Instances")
}

assert.NotEqual(t, 0, len(instanceEntries))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We often run tests in parallel, and I'm afraid if some other tests happens to fire up an EC2 instance in the same account, this test will pass, even if the code isn't actually working. Consider tagging the instances you launch from the aws-nuke test and make sure the instance you find with getAllEc2Instances includes the tag you expect.

In fact, it's probably a good idea to tag ALL resources created by aws-nuke tests with "aws-nuke-test-<UNIQUE_ID>" so if aws-nuke's own tests leave resources around, we know what the cause is. We typically use this UniqueId function to generate unique IDs for all our tests.

Copy link
Contributor Author

@tonerdo tonerdo Jan 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This particular test line simply checks that the creation of an instance succeeded and the next line ensures that the instance created by the test is present in the returned list. If in the TestNukeInstances we ensure we only delete appropriately tagged instances then, I feel this line will be fine

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider the following:

  1. We have two automated tests running in the same AWS account at the same time. The first test, foo, creates an EC2 instance. The second test, this one for aws-nuke, tries to create the EC2 Instance, but due to some bug in the test, fails.
  2. Now you do your check, which is merely seeing that there are more than 0 instances created. The check will pass, as foo created an EC2 Instance, but it should fail, as the instance created by aws-nuke didn't actually succeed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah makes sense, thanks for the clarification. I've removed this line and I now specifically check for the presence of the instance that was created

aws/ec2_test.go Outdated
}

func TestNukeInstances(t *testing.T) {
session, _ := session.NewSession(&awsgo.Config{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error handling

aws/ec2_test.go Outdated
assert.Fail(t, "Unable to fetch list of EC2 Instances")
}

assert.Equal(t, 0, len(instanceEntries))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work if other things are deploying into the same account or this test is running multiple times in parallel. See my note on tagging above.

commands/cli.go Outdated
app.HelpName = app.Name
app.Author = "Gruntwork <www.gruntwork.io>"
app.Version = version
app.Usage = "A CLI tool to cleanup AWS resources"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should definitely have a big warning text in the usage description!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As well as a list of what resources are currently supported

commands/cli.go Outdated
logging.Logger.Infoln(resource)
}

prompt := "\nAre you sure you want to nuke all listed resources"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a big, huge WARNING here. Yell at the user to tell them this is irreversible! Make the warning red (example of using colors on the CLI here).

@tonerdo
Copy link
Contributor Author

tonerdo commented Jan 24, 2018

Add a big, huge WARNING here. Yell at the user to tell them this is irreversible! Make the warning red (example of using colors on the CLI here).

The text is printed by the PromptUserForYesNo function which defaults to BRIGHT_GREEN with no way of specifying a custom color

@brikis98
Copy link
Member

The text is printed by the PromptUserForYesNo function which defaults to BRIGHT_GREEN with no way of specifying a custom color

Sure, but you can add a separate logging statement just before that in red! This is a hugely dangerous tools, so we need to very seriously warn users. In fact, even "yes/no" might not be the right prompt here. We might want to force users to type "nuke" or something where you can't possibly do it by accident.

@tonerdo
Copy link
Contributor Author

tonerdo commented Jan 24, 2018

We might want to force users to type "nuke" or something where you can't possibly do it by accident.

Good idea!

Copy link
Member

@brikis98 brikis98 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, almost there. Just some nitpicky code maintainability stuff to clean up 👍

* Deleting all unprotected EC2 instances in an AWS account

### WARNING: THIS TOOL IS HIGHLY DESTRUCTIVE, ALL SUPPORTED RESOURCES WILL BE DELETED. ITS EFFECTS ARE IRREVERSIBLE AND SHOULD NEVER BE USED IN A PRODUCTION ENVIRONMENT
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

README.md Outdated

1. Download the latest binary for your OS on the [releases page](https://github.com/gruntwork-io/aws-nuke/releases).
2. Move the binary to a folder on your `PATH`. E.g.: `mv gruntwork_darwin_amd64 /usr/local/bin/aws-nuke`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/gruntwork_darwin_amd64/aws-nuke_darwin_amd64/

aws/aws.go Outdated
}

// GetAllResources - Lists all aws resources
func GetAllResources() (map[string]map[string][]*string, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, that's quite a nested return value. Very hard to know what'll be in it! I had to read through all the code to figure out that it's a map of resource type -> map of region -> resource unique ID. I think.

This is a perfect opportunity to create some custom struct types with named parameters. Example:

type AwsAccountResources {
  ResourcesInRegion map[string]AwsRegionResources 
}

type AwsRegionResources struct {
  Instances []Ec2Instance
}

type Ec2Instance struct {
  InstanceId string
}

func GetAllResources() (*AwsAccountResources, error) {

}

Now I can read the function signature and types and know exactly what is being returned.

aws/aws.go Outdated
}

// NukeAllResources - Nukes all aws resources
func NukeAllResources(resourceMaps map[string]map[string][]*string) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here. To someone who isn't intimately familiar with the code, there's no way to guess what this map of maps should contain. See my comment above on using types to make this easier to understand.

aws/aws.go Outdated
}

err = nukeAllEc2Instances(session, resourceMaps["ec2"][region])
if err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In go, you can combine these into one statement:

if err := nukeAllEc2Instances(session, resourceMaps["ec2"][region]); err != nil {

}


// Add test tag to the created instance
_, err = svc.CreateTags(&ec2.CreateTagsInput{
Resources: []*string{runResult.Instances[0].InstanceId},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if, due to AWS eventual consistency, this API call can fail, as the information about this instance ID has not yet propagated... You may need to call WaitUntilInstanceExists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

aws/ec2_test.go Outdated
"github.com/stretchr/testify/assert"
)

var uniqueTestID = "aws-nuke-test-" + uniqueID()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create this variable within each test function and not out here, as multiple test functions may run in parallel, and should each have their own unique IDs.

In fact, as a general rule, avoid any sort of global state by default!

aws/ec2_test.go Outdated
assert.Fail(t, errors.WithStackTrace(err).Error())
}

output, err := ec2.New(session).DescribeInstances(&ec2.DescribeInstancesInput{})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code here, plus the for-loop below, should be extracted into a method called findEC2InstancesByNameTag(name string) to make it easier to read this code.

aws/ec2_test.go Outdated
}
}

nukeAllEc2Instances(session, instanceIds)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check for an error returned by this method!

commands/cli.go Outdated
}
}

prompt := "\nAre you sure you want to nuke all listed resources? Enter 'nuke' to confirm: "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would still add a log line before this with a big red warning. It's very important to get the user's attention here.

Copy link
Member

@brikis98 brikis98 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, one last change and we're there :)

aws/aws.go Outdated
ResourceIdentifiers: awsgo.StringValueSlice(instanceIds),
}

account.Resources["ec2"] = append(account.Resources["ec2"], regionResources)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I like having a map with arbitrary resource strings in it. It won't be obvious to a maintainer later what kind of resources may be in the map and it's prone to typos ("EC2" vs "ec2").

I'd recommend using the type system here and having lists of specific resources types, such as EC2Instance and (in the future), ASG, ELB, etc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, in fact, given your need to loop over all the resources and nuke them, it probably makes sense to do something like this:

interface AwsResources {
  Nuke(session *aws.Session) error 
}

type AwsRegionResources struct {
	RegionName         string
	Resources          []AwsResources
}

type EC2Instances struct {
  InstanceIds []string
}

// Implement the AwsResources interface
func (instance EC2Instance) Nuke(session *aws.Session) error {
    // This is what's currently in the nukeAllEc2Instances method
}

Now NukeAllResources can just loop over all the resources:

for _, region := range getAllRegions() {
  resourcesInRegion := account.Resources[region]
  for _, resources := resourcesInRegion.Resources {
    if err := resources.Nuke(session); err != nil {
      // ...
    }
  }
}

Copy link
Member

@brikis98 brikis98 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Merging! 👍

@brikis98 brikis98 merged commit 698207e into master Jan 25, 2018
@tonerdo tonerdo deleted the v0.0.1 branch February 12, 2018 12:17
ghost referenced this pull request Jul 13, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants