-
-
Notifications
You must be signed in to change notification settings - Fork 356
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
Conversation
There was a problem hiding this 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: |
There was a problem hiding this comment.
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:
-
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.
-
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`. |
There was a problem hiding this comment.
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 ./... |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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", | ||
} |
There was a problem hiding this comment.
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{ |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider the following:
- 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 foraws-nuke
, tries to create the EC2 Instance, but due to some bug in the test, fails. - 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 byaws-nuke
didn't actually succeed.
There was a problem hiding this comment.
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{ |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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).
The text is printed by the |
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. |
Good idea! |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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`. |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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}, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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{}) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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: " |
There was a problem hiding this comment.
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.
There was a problem hiding this 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 {
// ...
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Merging! 👍
This PR contains a reimplementation of the original aws-nuke bash shell script in golang.
Changes Include: