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

terminate using tags #37

Merged
merged 2 commits into from
Feb 5, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions cmd/terminate.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"simple-ec2/pkg/cli"
"simple-ec2/pkg/ec2helper"
"simple-ec2/pkg/question"
"simple-ec2/pkg/tag"

"github.com/aws/aws-sdk-go/aws/session"
"github.com/spf13/cobra"
Expand All @@ -29,7 +30,7 @@ import (
var terminateCmd = &cobra.Command{
Use: "terminate",
Short: "Terminate Amazon EC2 Instances",
Long: `Terminate Amazon EC2 Instances, given the region and instance ids`,
Long: `Terminate Amazon EC2 Instances, given the region and instance ids or tag values`,
pdk27 marked this conversation as resolved.
Show resolved Hide resolved
Run: terminate,
}

Expand All @@ -42,6 +43,8 @@ func init() {
terminateCmd.Flags().StringSliceVarP(&instanceIdFlag, "instance-ids", "n", nil,
"The instance ids of the instances you want to terminate")
terminateCmd.Flags().BoolVarP(&isInteractive, "interactive", "i", false, "Interactive mode")
terminateCmd.Flags().StringToStringVar(&flagConfig.UserTags, "tags", nil,
"Terminate instances containing EXACT tag key-pair (Example: CreatedBy=simple-ec2)")
brycahta marked this conversation as resolved.
Show resolved Hide resolved
}

// The main function
Expand Down Expand Up @@ -111,20 +114,21 @@ func terminateNonInteractive(h *ec2helper.EC2Helper) {
instanceIdFlag[i] = strings.TrimSpace(instanceIdFlag[i])
}

cli.ShowError(h.TerminateInstances(instanceIdFlag), "Terminating instances failed")
instFilters, err := tag.GetTagAsFilter(flagConfig.UserTags)
instancesToTerm, err := h.GetInstancesByFilter(instanceIdFlag, instFilters)
if err != nil {
cli.ShowError(err, "Finding instances with filters failed")
return
}

cli.ShowError(h.TerminateInstances(instancesToTerm), "Terminating instances failed")
brycahta marked this conversation as resolved.
Show resolved Hide resolved
}

// Validate flags using some simple rules. Return true if the flags are validated, false otherwise
func ValidateTerminateFlags() bool {
if !isInteractive && instanceIdFlag == nil && regionFlag == "" {
fmt.Println("Not in interactive mode and no flag is specified")
if !isInteractive && instanceIdFlag == nil && len(flagConfig.UserTags) == 0 {
brycahta marked this conversation as resolved.
Show resolved Hide resolved
fmt.Println("Specify instanceIds, tags, or use interactive mode")
pdk27 marked this conversation as resolved.
Show resolved Hide resolved
return false
}

if !isInteractive && instanceIdFlag == nil {
fmt.Println("Not in interactive mode and instance ids are not specified")
return false
}

return true
}
26 changes: 25 additions & 1 deletion pkg/ec2helper/ec2helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,31 @@ func (h *EC2Helper) GetInstancesByState(states []string) ([]*ec2.Instance, error
return instances, nil
}

func (h *EC2Helper) GetInstancesByFilter(instanceIds []string, filters []*ec2.Filter) ([]string, error) {
input := &ec2.DescribeInstancesInput{}
if len(instanceIds) > 0 {
input.InstanceIds = aws.StringSlice(instanceIds)
}
if len(filters) > 0 {
input.Filters = filters
}

instances, err := h.getInstances(input)
if err != nil {
return nil, err
}
if len(instances) <= 0 {
return nil, nil
}

var result []string
for _, instance := range instances {
result = append(result, *instance.InstanceId)
}

return result, nil
}

// Create tags for the resources specified
func (h *EC2Helper) createTags(resources []string, tags []*ec2.Tag) error {
input := &ec2.CreateTagsInput{
Expand Down Expand Up @@ -1239,7 +1264,6 @@ func GetTagName(tags []*ec2.Tag) *string {
return tag.Value
}
}

return nil
}

Expand Down
71 changes: 70 additions & 1 deletion pkg/ec2helper/ec2helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -963,7 +963,7 @@ func TestGetInstanceById_Success(t *testing.T) {
Instances: testInstances,
}

instance, err := testEC2.GetInstanceById("")
brycahta marked this conversation as resolved.
Show resolved Hide resolved
instance, err := testEC2.GetInstanceById(testInstanceId)
if err != nil {
t.Errorf(th.UnexpectedErrorFormat, err)
} else if *instance.InstanceId != testInstanceId {
Expand Down Expand Up @@ -1014,6 +1014,75 @@ func TestGetInstancesByState_Success(t *testing.T) {
}
}

func TestGetInstancesByFilter_Success(t *testing.T) {
testTags := []*ec2.Tag{
{
Key: aws.String("TestedBy"),
Value: aws.String("meh"),
},
}
testInstances := []*ec2.Instance{
{
InstanceId: aws.String("i-12345"),
Tags: testTags,
},
{
InstanceId: aws.String("i-67890"),
},
}
testEC2.Svc = &th.MockedEC2Svc{
Instances: testInstances,
}

testFilters := []*ec2.Filter{
{
Name: aws.String("tag:TestedBy"),
Values: aws.StringSlice([]string{"meh"}),
},
}

instances, err := testEC2.GetInstancesByFilter([]string{"i-12345", "i-67890"}, testFilters)
brycahta marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
t.Errorf(th.UnexpectedErrorFormat, err)
}
if len(instances) != 1 {
brycahta marked this conversation as resolved.
Show resolved Hide resolved
t.Error("Incorrect instance(s) returned after filtering")
fmt.Printf("instances: %v\n", instances)
} else {
if instances[0] != "i-12345" {
t.Error("Incorrect instance(s) returned after filtering")
}
}
}

func TestGetInstancesByFilter_NoResults(t *testing.T) {
testInstances := []*ec2.Instance{
{
InstanceId: aws.String("i-12345"),
},
{
InstanceId: aws.String("i-67890"),
},
}
testEC2.Svc = &th.MockedEC2Svc{
Instances: testInstances,
}

testFilters := []*ec2.Filter{
{
Name: aws.String("tag:TestedBy"),
Values: aws.StringSlice([]string{"meh"}),
},
}

instances, err := testEC2.GetInstancesByFilter([]string{"i-12345", "i-67890"}, testFilters)
if err != nil {
t.Errorf(th.UnexpectedErrorFormat, err)
} else if len(instances) != 0 {
t.Error("Instances should NOT have been returned after filtering")
}
}

func TestGetInstancesByState_DescribeInstancesPagesError(t *testing.T) {
testEC2.Svc = &th.MockedEC2Svc{
DescribeInstancesPagesError: errors.New("Test error"),
Expand Down
2 changes: 1 addition & 1 deletion pkg/question/question.go
Original file line number Diff line number Diff line change
Expand Up @@ -1015,7 +1015,7 @@ func AskConfirmationWithInput(simpleConfig *config.SimpleInfo, detailedConfig *c
if simpleConfig.BootScriptFilePath != "" {
data = append(data, []string{cli.ResourceBootScriptFilePath, simpleConfig.BootScriptFilePath})
}
if simpleConfig.UserTags != nil {
if len(simpleConfig.UserTags) != 0 {
var tags []string
for k, v := range simpleConfig.UserTags {
tags = append(tags, fmt.Sprintf("%s|%s", k, v))
Expand Down
14 changes: 14 additions & 0 deletions pkg/tag/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ package tag
import (
"fmt"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
)

// Get the tags for resources created by simple-ec2
Expand All @@ -31,3 +34,14 @@ func GetSimpleEc2Tags() *map[string]string {
}
return &tags
}

// Convert tag map to Filter
func GetTagAsFilter(userTags map[string]string) (filters []*ec2.Filter, err error) {
for k, v := range userTags {
filters = append(filters, &ec2.Filter{
Name: aws.String("tag:" + k), //prepend tag: for exact matching
pdk27 marked this conversation as resolved.
Show resolved Hide resolved
Values: aws.StringSlice([]string{v}),
})
}
return filters, nil
}
36 changes: 35 additions & 1 deletion pkg/tag/tag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@
package tag_test

import (
"fmt"
"regexp"
"testing"

"simple-ec2/pkg/tag"
)

const createTimeRegex = "^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2} [A-Z]{3}"

func TestGetTags(t *testing.T) {
tags := tag.GetSimpleEc2Tags()

Expand All @@ -27,8 +31,38 @@ func TestGetTags(t *testing.T) {
t.Error("CreatedBy tag is not created correctly")
}

_, found = (*tags)["CreatedTime"]
createdTime, found := (*tags)["CreatedTime"]
matched, _ := regexp.MatchString(createTimeRegex, createdTime)
if !matched {
t.Error("CreatedTime tag was not created/formatted correctly")
}
if !found {
t.Error("CreatedTime tag is not created correctly")
}
}

func TestGetTagAsFilter(t *testing.T) {
tags := tag.GetSimpleEc2Tags()
actualTagFilter, err := tag.GetTagAsFilter(*tags)
if err != nil {
t.Error("GetTagAsFilter encountered an error: " + err.Error())
}

if len(actualTagFilter) != 2 {
t.Error("TagFilters length should be 2 but is " + fmt.Sprint(len(actualTagFilter)))
}
for _, tfilter := range actualTagFilter {
if *tfilter.Name != "tag:CreatedBy" && *tfilter.Name != "tag:CreatedTime" {
t.Error("TagFilters does not contain tag:CreatedBy or tag:CreatedTime")
}
if *tfilter.Name == "tag:CreatedBy" && *tfilter.Values[0] != "simple-ec2" {
t.Error("CreatedBy tag was not converted to filter correctly")
}
if *tfilter.Name == "tag:CreatedTime" {
matched, _ := regexp.MatchString(createTimeRegex, *tfilter.Values[0])
if !matched {
t.Error("CreatedTime tag was not converted to filter correctly")
}
}
}
}
26 changes: 25 additions & 1 deletion test/testhelper/ec2helper_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package testhelper

import (
"strconv"
"strings"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
Expand Down Expand Up @@ -303,10 +304,33 @@ func (e *MockedEC2Svc) AuthorizeSecurityGroupIngress(input *ec2.AuthorizeSecurit
}

func (e *MockedEC2Svc) DescribeInstancesPages(input *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool) error {
var instances []*ec2.Instance
// mock filtering
for _, inst := range e.Instances {
addToInstances := true
for _, filter := range input.Filters {
// supports tag:<key> filter
if strings.Contains(*filter.Name, "tag:") {
tagName := strings.Split(*filter.Name, ":")[1]
pdk27 marked this conversation as resolved.
Show resolved Hide resolved
tagVal := *filter.Values[0]
if len(inst.Tags) == 0 {
addToInstances = false
break
}
if *inst.Tags[0].Key != tagName || *inst.Tags[0].Value != tagVal {
addToInstances = false
break
}
}
}
if addToInstances {
instances = append(instances, inst)
}
}
output := &ec2.DescribeInstancesOutput{
Reservations: []*ec2.Reservation{
{
Instances: e.Instances,
Instances: instances,
},
},
}
Expand Down