diff --git a/golang_auto_start_stop_ec2/ec2sched/.DS_Store b/golang_auto_start_stop_ec2/ec2sched/.DS_Store new file mode 100644 index 0000000..7adcd08 Binary files /dev/null and b/golang_auto_start_stop_ec2/ec2sched/.DS_Store differ diff --git a/golang_auto_start_stop_ec2/ec2sched/AutoStart/main.go b/golang_auto_start_stop_ec2/ec2sched/AutoStart/main.go new file mode 100644 index 0000000..5b894fc --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/AutoStart/main.go @@ -0,0 +1,154 @@ +package main + +import ( + "fmt" + "go/ec2sched/pkg/sess" + "os" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" +) + +type startInst interface { + startInst(instId string) (string, error) +} + +type instInfo interface { + instInfo(tagName string) (*ec2.DescribeInstancesOutput, error) +} + +type ec2Api struct { + Client ec2iface.EC2API +} + +// Fetch instances with tag "AutoSart", which is passed as input parameter +func (e ec2Api) instInfo(tagName string) (*ec2.DescribeInstancesOutput, error) { + + var maxOutput int = 75 + m := int64(maxOutput) + var resp *ec2.DescribeInstancesOutput + + input := &ec2.DescribeInstancesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("tag-key"), + Values: []*string{ + aws.String(tagName), + }, + }, + }, + MaxResults: &m, + } + + //Cycle through paginated results for describe instances (incase we have more than 75 instances) + for { + instOutput, err := e.Client.DescribeInstances(input) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + return nil, awsErr + } + return nil, err + } + + if resp == nil { + resp = instOutput + } else { + resp.Reservations = append(resp.Reservations, instOutput.Reservations...) + } + + if instOutput.NextToken == nil { + break + } + + input.NextToken = instOutput.NextToken + } + //fmt.Println(resp) + return resp, nil + +} + +// Evaluate instances to see if we can start. +func evalInst(inst *ec2.Instance, s startInst) (string, error) { + + fmt.Println(*inst.InstanceId) + + for _, tag := range inst.Tags { + //ASGs not supported + if *tag.Key == "aws:autoscaling:groupName" { + return fmt.Sprintf("Skipping - %s is part of autoscaling group %s", string(*inst.InstanceId), *tag.Value), nil + } + } + + result, err := s.startInst(*inst.InstanceId) + if err != nil { + return "", err + } + return "Starting Instance: " + result, nil +} + +func (e ec2Api) startInst(instId string) (string, error) { + + input := &ec2.StartInstancesInput{ + InstanceIds: []*string{ + aws.String(instId), + }, + } + + res, si_err := e.Client.StartInstances(input) + if si_err != nil { + if awsErr, ok := si_err.(awserr.Error); ok { + fmt.Println(awsErr.Error()) + return "", awsErr + } else { + fmt.Println(si_err.Error()) + return "", si_err + } + } + return *res.StartingInstances[0].InstanceId, nil +} + +func HandleLambdaEvent() { + + //Tag to look for on the instances + schedule_tag := "autostart" + region := os.Getenv("REGION_TZ") + sess, err := sess.EstablishSession(region) + e := ec2Api{ + Client: ec2.New(sess), + } + + instDesc, err := e.instInfo(schedule_tag) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + instCount := 0 + + for idx, res := range instDesc.Reservations { + instCount += len(res.Instances) + + for _, inst := range instDesc.Reservations[idx].Instances { + if *inst.State.Name != "running" { + st, err := evalInst(inst, e) + if err != nil { + fmt.Println(err.Error()) + } + fmt.Println(st) + + } else { + fmt.Printf("Instance: %s is already running\n", *inst.InstanceId) + } + } + fmt.Println("-----") + } + fmt.Printf("Instance count evaluated with %s tag: %d", schedule_tag, instCount) + return +} + +func main() { + lambda.Start(HandleLambdaEvent) +} diff --git a/golang_auto_start_stop_ec2/ec2sched/AutoStart/main_test.go b/golang_auto_start_stop_ec2/ec2sched/AutoStart/main_test.go new file mode 100644 index 0000000..8ac09ee --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/AutoStart/main_test.go @@ -0,0 +1,196 @@ +package main + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/stretchr/testify/assert" +) + +type MockEC2Client struct { + ec2iface.EC2API + DescribeInstancesOutputValue ec2.DescribeInstancesOutput + StartInstancesOutputValue ec2.StartInstancesOutput +} + +type mockStartInst struct{} + +func (m *MockEC2Client) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) { + return &m.DescribeInstancesOutputValue, nil +} + +func (m *MockEC2Client) StartInstances(input *ec2.StartInstancesInput) (*ec2.StartInstancesOutput, error) { + return &m.StartInstancesOutputValue, nil +} + +func (m mockStartInst) startInst(instId string) (string, error) { + return "i-0c938b5e573fb0f26", nil +} + +var outputZero = ec2.DescribeInstancesOutput{} +var outputWithAutostart = ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("autostart"), + Value: aws.String("true"), + }, + { + Key: aws.String("autostop"), + Value: aws.String("true"), + }, + }, + }, + { + InstanceId: aws.String("i-0c938b5e573fb0f27"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameRunning), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("autostart"), + Value: aws.String("true"), + }, + { + Key: aws.String("aws:autoscaling:groupName"), + Value: aws.String("test-asg"), + }, + }, + }, + }, + }, + }, +} + +func TestInstInfo(t *testing.T) { + + //Resp is the result from the ec2 api + //Expected is the expected result from the function instInfo + cases := []struct { + Name string + Resp ec2.DescribeInstancesOutput + Expected ec2.DescribeInstancesOutput + }{ + { + Name: "AutostartPresent", + Resp: outputWithAutostart, + Expected: outputWithAutostart, + }, + { + Name: "Zero EC2s", + Resp: outputZero, + Expected: outputZero, + }, + } + + //time to iterate over the test cases + for _, c := range cases { + + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + e := ec2Api{ + Client: &MockEC2Client{ + EC2API: nil, + DescribeInstancesOutputValue: c.Resp, + }, + } + inst, err := e.instInfo("autostop") + + if err != nil { + fmt.Println("Unexpected Error - ", err.Error()) + } + + assert.Equal(t, "*ec2.DescribeInstancesOutput", fmt.Sprintf("%T", inst)) + + }) + } +} + +func TestEvalInst(t *testing.T) { + + //Resp is the result from the ec2 api StartInstances + cases := []struct { + Name string + InstanceInput *ec2.Instance + Expected string + }{ + { + Name: "Autoscaling group present", + InstanceInput: outputWithAutostart.Reservations[0].Instances[1], + Expected: "Skipping - i-0c938b5e573fb0f27 is part of autoscaling group test-asg", + }, + { + Name: "Autoscaling group not present", + InstanceInput: outputWithAutostart.Reservations[0].Instances[0], + Expected: "Starting Instance: i-0c938b5e573fb0f26", + }, + } + + //time to iterate over the test cases + for _, c := range cases { + + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + s := mockStartInst{} + st, err := evalInst(c.InstanceInput, s) + + if err != nil { + fmt.Println("Unexpected Error - ", err.Error()) + } + + assert.Equal(t, c.Expected, st) + + }) + } + +} + +func TestStartInst(t *testing.T) { + + //Resp is the result from the ec2 api StartInstances + cases := []struct { + Name string + Resp ec2.StartInstancesOutput + Expected string + }{ + { + Name: "StartInstancesOutput", + Resp: ec2.StartInstancesOutput{StartingInstances: []*ec2.InstanceStateChange{{InstanceId: aws.String("i-0c938b5e573fb0f26")}}}, + Expected: "i-0c938b5e573fb0f26", + }, + } + + //time to iterate over the test cases + for _, c := range cases { + + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + e := ec2Api{ + Client: &MockEC2Client{ + EC2API: nil, + StartInstancesOutputValue: c.Resp, + }, + } + inst, err := e.startInst("i-0c938b5e573fb0f26") + + if err != nil { + fmt.Println("Unexpected Error - ", err.Error()) + } + + assert.Equal(t, c.Expected, inst) + + }) + } +} diff --git a/golang_auto_start_stop_ec2/ec2sched/AutoStop/main.go b/golang_auto_start_stop_ec2/ec2sched/AutoStop/main.go new file mode 100644 index 0000000..2f0ea17 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/AutoStop/main.go @@ -0,0 +1,152 @@ +package main + +import ( + "fmt" + "go/ec2sched/pkg/sess" + "os" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" +) + +type stoptInst interface { + stopInst(instId string) (string, error) +} + +type instInfo interface { + instInfo(tagName string) (*ec2.DescribeInstancesOutput, error) +} + +type ec2Api struct { + Client ec2iface.EC2API +} + +// Fetch instances with tag "AutoStop", which is passed as input parameter +func (e ec2Api) instInfo(tagName string) (*ec2.DescribeInstancesOutput, error) { + var maxOutput int = 75 + m := int64(maxOutput) + var resp *ec2.DescribeInstancesOutput + + input := &ec2.DescribeInstancesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("tag-key"), + Values: []*string{ + aws.String(tagName), + }, + }, + }, + MaxResults: &m, + } + + //Cycle through paginated results for describe instances (incase we have more than 75 instances) + for { + instOutput, err := e.Client.DescribeInstances(input) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + return nil, awsErr + } + return nil, err + } + + if resp == nil { + resp = instOutput + } else { + resp.Reservations = append(resp.Reservations, instOutput.Reservations...) + } + + if instOutput.NextToken == nil { + break + } + + input.NextToken = instOutput.NextToken + } + //fmt.Println(resp) + return resp, nil +} + +// Evaluate instances to see if we can stop. +func evalInst(inst *ec2.Instance, s stoptInst) (string, error) { + + fmt.Println(*inst.InstanceId) + + for _, tag := range inst.Tags { + //ASGs not supported + if *tag.Key == "aws:autoscaling:groupName" { + return fmt.Sprintf("Skipping - %s is part of autoscaling group %s", string(*inst.InstanceId), *tag.Value), nil + } + } + + result, err := s.stopInst(*inst.InstanceId) + if err != nil { + return "", err + } + return "Stopping Instance: " + result, nil +} + +func (e ec2Api) stopInst(instId string) (string, error) { + + input := &ec2.StopInstancesInput{ + InstanceIds: []*string{ + aws.String(instId), + }, + } + + res, si_err := e.Client.StopInstances(input) + if si_err != nil { + if awsErr, ok := si_err.(awserr.Error); ok { + fmt.Println(awsErr.Error()) + return "", awsErr + } else { + fmt.Println(si_err.Error()) + return "", si_err + } + } + return *res.StoppingInstances[0].InstanceId, nil +} + +func HandleLambdaEvent() { + + //Tag too look for on the instances + schedule_tag := "autostop" + region := os.Getenv("REGION_TZ") + sess, err := sess.EstablishSession(region) + e := ec2Api{ + Client: ec2.New(sess), + } + + instDesc, err := e.instInfo(schedule_tag) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + instCount := 0 + + for idx, res := range instDesc.Reservations { + instCount += len(res.Instances) + + for _, inst := range instDesc.Reservations[idx].Instances { + if *inst.State.Name != "stopped" { + st, err := evalInst(inst, e) + if err != nil { + fmt.Println(err.Error()) + } + fmt.Println(st) + + } else { + fmt.Printf("Instance: %s is already stopped\n", *inst.InstanceId) + } + } + fmt.Println("-----") + } + fmt.Printf("Instance count evaluated with %s tag: %d", schedule_tag, instCount) + return +} + +func main() { + lambda.Start(HandleLambdaEvent) +} diff --git a/golang_auto_start_stop_ec2/ec2sched/AutoStop/main_test.go b/golang_auto_start_stop_ec2/ec2sched/AutoStop/main_test.go new file mode 100644 index 0000000..5358960 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/AutoStop/main_test.go @@ -0,0 +1,197 @@ +package main + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/stretchr/testify/assert" +) + +type MockEC2Client struct { + ec2iface.EC2API + DescribeInstancesOutputValue ec2.DescribeInstancesOutput + StopInstancesOutputValue ec2.StopInstancesOutput +} + +type mockStopInst struct{} + +func (m *MockEC2Client) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) { + return &m.DescribeInstancesOutputValue, nil +} + +func (m *MockEC2Client) StopInstances(input *ec2.StopInstancesInput) (*ec2.StopInstancesOutput, error) { + return &m.StopInstancesOutputValue, nil +} + +func (m mockStopInst) stopInst(instId string) (string, error) { + return "i-0c938b5e573fb0f26", nil +} + +// Output for mocked DescribeInstancesOutput from DesribeInstances EC2 api call +var outputZero = ec2.DescribeInstancesOutput{} +var outputWithAutostop = ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("autostart"), + Value: aws.String("true"), + }, + { + Key: aws.String("autostop"), + Value: aws.String("true"), + }, + }, + }, + { + InstanceId: aws.String("i-0c938b5e573fb0f27"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameRunning), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("autostop"), + Value: aws.String("true"), + }, + { + Key: aws.String("aws:autoscaling:groupName"), + Value: aws.String("test-asg"), + }, + }, + }, + }, + }, + }, +} + +func TestInstInfo(t *testing.T) { + + //Resp is the result from the ec2 api + //Expected is the expected result from the function instInfo + cases := []struct { + Name string + Resp ec2.DescribeInstancesOutput + Expected ec2.DescribeInstancesOutput + }{ + { + Name: "AutostopPresent", + Resp: outputWithAutostop, + Expected: outputWithAutostop, + }, + { + Name: "NoInstances", + Resp: outputZero, + Expected: outputZero, + }, + } + + //time to iterate over the test cases + for _, c := range cases { + + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + e := ec2Api{ + Client: &MockEC2Client{ + EC2API: nil, + DescribeInstancesOutputValue: c.Resp, + }, + } + inst, err := e.instInfo("autostop") + + if err != nil { + fmt.Println("Unexpected Error - ", err.Error()) + } + + assert.Equal(t, "*ec2.DescribeInstancesOutput", fmt.Sprintf("%T", inst)) + + }) + } +} + +func TestEvalInst(t *testing.T) { + + //Resp is the result from the ec2 api StartInstances + cases := []struct { + Name string + InstanceInput *ec2.Instance + Expected string + }{ + { + Name: "Autoscaling group present", + InstanceInput: outputWithAutostop.Reservations[0].Instances[1], + Expected: "Skipping - i-0c938b5e573fb0f27 is part of autoscaling group test-asg", + }, + { + Name: "Autoscaling group not present", + InstanceInput: outputWithAutostop.Reservations[0].Instances[0], + Expected: "Stopping Instance: i-0c938b5e573fb0f26", + }, + } + + //time to iterate over the test cases + for _, c := range cases { + + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + s := mockStopInst{} + st, err := evalInst(c.InstanceInput, s) + + if err != nil { + fmt.Println("Unexpected Error - ", err.Error()) + } + + assert.Equal(t, c.Expected, st) + + }) + } + +} + +func TestStartInst(t *testing.T) { + + //Resp is the result from the ec2 api StartInstances + cases := []struct { + Name string + Resp ec2.StopInstancesOutput + Expected string + }{ + { + Name: "StopInstancesOutput", + Resp: ec2.StopInstancesOutput{StoppingInstances: []*ec2.InstanceStateChange{{InstanceId: aws.String("i-0c938b5e573fb0f26")}}}, + Expected: "i-0c938b5e573fb0f26", + }, + } + + //time to iterate over the test cases + for _, c := range cases { + + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + e := ec2Api{ + Client: &MockEC2Client{ + EC2API: nil, + StopInstancesOutputValue: c.Resp, + }, + } + inst, err := e.stopInst("i-0c938b5e573fb0f26") + + if err != nil { + fmt.Println("Unexpected Error - ", err.Error()) + } + + assert.Equal(t, c.Expected, inst) + + }) + } +} diff --git a/golang_auto_start_stop_ec2/ec2sched/README.md b/golang_auto_start_stop_ec2/ec2sched/README.md new file mode 100644 index 0000000..b3b2dc5 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/README.md @@ -0,0 +1,66 @@ +# ec2sched + +Containing lambda code for AutoStart/AutoStop, StartWeekDay/StopWeekDay + +Asgsched allows for customizable Start Stop schedules to fit a variaty of use cases. The code evaluates tags on the autoscaling groups determining actions. + +Code Info ++ Code base language [go1.20.1](https://go.dev/doc/) +
+
+ +## Schedule Functions + +The sheduling lambda functions operate by in large the same way. Collects the autoscaling groups, views the corresponding tag, and takes actions based on the tag value. +> [!NOTE] +> Tag names and values are case sensitive. + +If action is needed the autoscaling group actions are suspended (Launch, Terminate, Alarm Notification, etc.) then the underlying instances are stopped, the inverse happens when autoscaling groups need to start: underlying autoscaling group instance are started, then autoscaling actions are resumed. Fuction specifics outlined below + +
+
+ +### AutoStart/AutoStop + +AutoStart/AutoStop functions look for the AutoSart/AutoStop tag on the autoscaling group. If the tag (AutoStart/AutoStop) exists and value is `true` the function takes action. + +The deployed lambda for AutoStart and AutoStop is typically triggered via eventbridge rules running at the desired times + +Deploy Specifics ++ Tag Names + - AutoStart + - AutoStop ++ Tag Values + - `true` + - If that tag value is blank or a value other than `true` no action will take place. + +
+
+ +### Start/Stop and WeekDay/WeekEnd + +Start/Stop WeekDay/WeekEnd functions look for time values via tags on the autoscaling group. If that tag is present and the value (time) is plus or minus 5 minutes of the function running, currently a weekday is (Monday-Friday according to TZ tag), the start stop action will take place. + +The deployed lambda for Start/Stop WeekDay/WeekEnd is intended to triggered via eventbridge rule which runs every 5min. + +Deploy Specifics ++ Tag Names + - startweekday + - stopweekday + - startweekend + - stopweekend ++ Tag Values + - 24 hour values i.e. 14:00 for 2pm ++ Environment Variables + - TZ: Timezone the specified time value will be assessed i.e. US/Pacific, US/Eastern, Europe/London **Required** + +--- +
+ +# Build Binaries + +There are different options for OS and architecture (GOOS & GOARCH). To build on linux for arm64 +`env GOOS=linux GOARCH=arm64 go build -o bootstrap main.go` +> [!NOTE] +> Be sure to set the proper lambda properties i.e. Runtime, Architectures, and Handler that work with your GOOS and GOARCH values. + diff --git a/golang_auto_start_stop_ec2/ec2sched/go.mod b/golang_auto_start_stop_ec2/ec2sched/go.mod new file mode 100644 index 0000000..e63e4c8 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/go.mod @@ -0,0 +1,17 @@ +module go/ec2sched + +go 1.20 + +require ( + github.com/aws/aws-sdk-go v1.44.298 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/aws/aws-lambda-go v1.41.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/golang_auto_start_stop_ec2/ec2sched/go.sum b/golang_auto_start_stop_ec2/ec2sched/go.sum new file mode 100644 index 0000000..c4c5ad2 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/go.sum @@ -0,0 +1,61 @@ +github.com/aws/aws-lambda-go v1.41.0 h1:l/5fyVb6Ud9uYd411xdHZzSf2n86TakxzpvIoz7l+3Y= +github.com/aws/aws-lambda-go v1.41.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM= +github.com/aws/aws-sdk-go v1.44.277 h1:YHmyzBPARTJ7LLYV1fxbfEbQOaUh3kh52hb7nBvX3BQ= +github.com/aws/aws-sdk-go v1.44.277/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.298 h1:5qTxdubgV7PptZJmp/2qDwD2JL187ePL7VOxsSh1i3g= +github.com/aws/aws-sdk-go v1.44.298/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/golang_auto_start_stop_ec2/ec2sched/pkg/sess/sess.go b/golang_auto_start_stop_ec2/ec2sched/pkg/sess/sess.go new file mode 100644 index 0000000..3e48206 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/pkg/sess/sess.go @@ -0,0 +1,22 @@ +package sess + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/session" +) + +func EstablishSession(region string) (*session.Session, error) { + sess, err := session.NewSession(&aws.Config{ + Region: aws.String(region), + }) + + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + return nil, awsErr + } + return nil, err + } + + return sess, nil +} diff --git a/golang_auto_start_stop_ec2/ec2sched/pkg/settz/settz.go b/golang_auto_start_stop_ec2/ec2sched/pkg/settz/settz.go new file mode 100644 index 0000000..3673347 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/pkg/settz/settz.go @@ -0,0 +1,20 @@ +package settz + +import ( + "os" + "time" +) + +func SetRegion(tz string) (string, error) { + if tz == "" { + tz = "UTC" + } + + loc, err := time.LoadLocation(tz) + if err != nil { + return "", err + } + + os.Setenv("TZ", loc.String()) + return loc.String(), nil +} diff --git a/golang_auto_start_stop_ec2/ec2sched/pkg/settz/settz_test.go b/golang_auto_start_stop_ec2/ec2sched/pkg/settz/settz_test.go new file mode 100644 index 0000000..8192d18 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/pkg/settz/settz_test.go @@ -0,0 +1,35 @@ +package settz + +import ( + "testing" +) + +func TestSetRegion(t *testing.T) { + + t.Run("Usecase 1", func(t *testing.T) { + result, _ := SetRegion("") + expected_res := "UTC" + + if result != expected_res { + t.Errorf("SetRegion(\"\") = %s; expected %s", result, expected_res) + } + }) + + t.Run("Usecase 2", func(t *testing.T) { + result, _ := SetRegion("America/Los_Angeles") + expected_res := "America/Los_Angeles" + + if result != expected_res { + t.Errorf("SetRegion(\"America/Los_Angeles\") = %s; expected %s", result, expected_res) + } + }) + + t.Run("Usecase 3", func(t *testing.T) { + _, err := SetRegion("America/Los_Angelos") + expected_err := "unknown time zone America/Los_Angelos" + + if err.Error() != expected_err { + t.Errorf("SetRegion(\"America/Los_Angelos\") = %s; expected %s", err.Error(), expected_err) + } + }) +} diff --git a/golang_auto_start_stop_ec2/ec2sched/startWeekDay/.DS_Store b/golang_auto_start_stop_ec2/ec2sched/startWeekDay/.DS_Store new file mode 100644 index 0000000..f93ad88 Binary files /dev/null and b/golang_auto_start_stop_ec2/ec2sched/startWeekDay/.DS_Store differ diff --git a/golang_auto_start_stop_ec2/ec2sched/startWeekDay/main.go b/golang_auto_start_stop_ec2/ec2sched/startWeekDay/main.go new file mode 100644 index 0000000..007f015 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/startWeekDay/main.go @@ -0,0 +1,195 @@ +package main + +import ( + "fmt" + "go/ec2sched/pkg/sess" + "go/ec2sched/pkg/settz" + "os" + "time" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" +) + +type startInst interface { + startInst(instId string, region string) (string, error) +} + +type instInfo interface { + instInfo(tagName string, region string) (*ec2.DescribeInstancesOutput, error) +} + +type ec2Api struct { + Client ec2iface.EC2API +} + +func (e ec2Api) instInfo(tagName string, region string) (*ec2.DescribeInstancesOutput, error) { + + var maxOutput int = 75 + m := int64(maxOutput) + var resp *ec2.DescribeInstancesOutput + + input := &ec2.DescribeInstancesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("tag-key"), + Values: []*string{ + aws.String(tagName), + }, + }, + }, + MaxResults: &m, + } + + //Cycle through paginated results for describe instances (incase we have more than 75 instances) + for { + instOutput, err := e.Client.DescribeInstances(input) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + return nil, awsErr + } + return nil, err + } + + if resp == nil { + resp = instOutput + } else { + resp.Reservations = append(resp.Reservations, instOutput.Reservations...) + } + + if instOutput.NextToken == nil { + break + } + + input.NextToken = instOutput.NextToken + } + //fmt.Println(resp) + return resp, nil + +} + +func (e ec2Api) startInst(instId string, region string) (string, error) { + input := &ec2.StartInstancesInput{ + InstanceIds: []*string{ + aws.String(instId), + }, + } + + res, si_err := e.Client.StartInstances(input) + if si_err != nil { + if awsErr, ok := si_err.(awserr.Error); ok { + fmt.Println(awsErr.Error()) + return "", awsErr + } else { + fmt.Println(si_err.Error()) + return "", si_err + } + } + //fmt.Println(res) + return *res.StartingInstances[0].InstanceId, nil +} + +func evalInst(inst *ec2.Instance, region string, curTime time.Time, s startInst) (string, error) { + dayOfWeek := curTime.Weekday() + modTime := curTime.Format(("15:04")) + + //startTime is the desired start time for the ec2 instance + startTime := "" + + fmt.Println(*inst.InstanceId) + + //Checks to see if weekend. If so skip out + if int(dayOfWeek) >= 6 && int(dayOfWeek) <= 7 { + return fmt.Sprintf("Current day of week %s. StartWeekDay requires non weekend values", dayOfWeek), nil + } + + for _, tag := range inst.Tags { + if *tag.Key == "StartWeekDay" { + //fmt.Println("StartWeekDay tag found. Value: ", *tag.Value) + startTime = *tag.Value + } + //ASGs not supported + if *tag.Key == "aws:autoscaling:groupName" { + return fmt.Sprintf("Skipping - %s is part of autoscaling group %s", string(*inst.InstanceId), *tag.Value), nil + } + } + + //Point here is to get the times on the same day to compare time of day, not just the date. Ensuring Apples to Apples + cur_tod, _ := time.Parse("15:04", modTime) + start_tod, start_tod_err := time.Parse("15:04", startTime) + + if start_tod_err != nil { + return "", start_tod_err + } + + cur_minus := cur_tod.Add(-time.Minute * 5) + cur_plus := cur_tod.Add(time.Minute * 5) + + if start_tod.After(cur_minus) && start_tod.Before(cur_plus) { + result, err := s.startInst(*inst.InstanceId, region) + if err != nil { + return "", err + } + return "Starting Instance: " + result, nil + } else { + strVal := string(*inst.InstanceId) + return "StartWeekDay schedule not matched for: " + strVal, nil + } +} + +// Handler doing bulk of work +// Environment variables set at the lambda configuration level +func HandleLambdaEvent() { + + //Tag to look for on the instance + schedule_tag := "startweekday" + tz, err := settz.SetRegion(os.Getenv("TZ")) + if err != nil { + fmt.Println(err.Error()) + } + fmt.Println(tz) + fmt.Println(time.Now()) + + region := os.Getenv("REGION_TZ") + + sess, _ := sess.EstablishSession(region) + e := ec2Api{ + Client: ec2.New(sess), + } + + instDesc, err := e.instInfo(schedule_tag, region) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + instCount := 0 + + for idx, res := range instDesc.Reservations { + instCount += len(res.Instances) + + for _, inst := range instDesc.Reservations[idx].Instances { + if *inst.State.Name != "running" { + st, err := evalInst(inst, region, time.Now(), e) + if err != nil { + fmt.Println(err.Error()) + } + fmt.Println(st) + + } else { + fmt.Printf("Instance: %s is already running\n", *inst.InstanceId) + } + } + fmt.Println("-----") + } + fmt.Printf("Instance count evaluated with %s tag: %d", schedule_tag, instCount) + return +} + +// Go must call main function first, so we call the handler from the main. +func main() { + lambda.Start(HandleLambdaEvent) +} diff --git a/golang_auto_start_stop_ec2/ec2sched/startWeekDay/main_test.go b/golang_auto_start_stop_ec2/ec2sched/startWeekDay/main_test.go new file mode 100644 index 0000000..ca01f48 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/startWeekDay/main_test.go @@ -0,0 +1,308 @@ +package main + +import ( + "fmt" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/stretchr/testify/assert" +) + +// Create a mock structure implementing the ec2iface interface. Makes the +// call to aws ec2 api +type MockEC2Client struct { + ec2iface.EC2API + DescribeInstancesOutputValue ec2.DescribeInstancesOutput + StartInstancesOutputValue ec2.StartInstancesOutput +} + +// Implements the interface +type mockStartInst struct{} + +// Implement the mocked DescribeInstances function +func (m *MockEC2Client) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) { + return &m.DescribeInstancesOutputValue, nil +} + +func (m *MockEC2Client) StartInstances(input *ec2.StartInstancesInput) (*ec2.StartInstancesOutput, error) { + return &m.StartInstancesOutputValue, nil +} + +func (m mockStartInst) startInst(instId string, region string) (string, error) { + return "i-0c938b5e573fb0f26", nil +} + +func TestInstInfo(t *testing.T) { + //Becauase we use the filter for included in the DescribeInstances we're testing to see + //we get a valid data type back + + //Response from ec2 aws api and the expected result for function instInfo + descInstancesWithStartWeekDay := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("startweekday"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("stopweekday"), + Value: aws.String("17:00"), + }, + }, + }, + }, + }, + }, + } + + cases := []struct { + Name string + Resp ec2.DescribeInstancesOutput + Expected ec2.DescribeInstancesOutput + }{ + { + Name: "StartWeekDayPresent", + Resp: descInstancesWithStartWeekDay, + Expected: descInstancesWithStartWeekDay, + }, + } + + //time to iterate over the test cases + for _, c := range cases { + + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + e := ec2Api{ + Client: &MockEC2Client{ + EC2API: nil, + DescribeInstancesOutputValue: c.Resp, + }, + } + inst, err := e.instInfo("startweekday", "us-east-1") + + if err != nil { + fmt.Println("Unexpected Error - ", err.Error()) + } + + assert.Equal(t, "*ec2.DescribeInstancesOutput", fmt.Sprintf("%T", inst)) + + }) + } +} + +func TestStartInst(t *testing.T) { + //Do we start the instance? What do we get back from the startInst function + + //The outputs we're mocking for the ec2 aws api for StartInstances + var ( + //Output when starting Instance + startInstancesOutput = ec2.StartInstancesOutput{ + StartingInstances: []*ec2.InstanceStateChange{ + { + InstanceId: aws.String("i-0c61fb5e573fb0c55"), + PreviousState: &ec2.InstanceState{ + Name: aws.String("stopped"), + }, + }, + }, + } + + //Output when instance is stopped + startInstancesOutputStopped = ec2.StartInstancesOutput{ + StartingInstances: []*ec2.InstanceStateChange{ + { + InstanceId: aws.String("i-0c61fb5e573fb0c55"), + PreviousState: &ec2.InstanceState{ + Name: aws.String("running"), + }, + }, + }, + } + ) + + cases := []struct { + Name string + Resp ec2.StartInstancesOutput + Expected string + }{ + { + Name: "ProperInstanceId", + Resp: startInstancesOutput, + Expected: "i-0c61fb5e573fb0c55", + }, + { + Name: "InstanceRunning", + Resp: startInstancesOutputStopped, + Expected: "i-0c61fb5e573fb0c55", + }, + } + + for _, c := range cases { + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + e := ec2Api{ + Client: &MockEC2Client{ + EC2API: nil, + StartInstancesOutputValue: c.Resp, + }, + } + startInst, err := e.startInst("i-0c61fb5e573fb0c55", "us-east-1") + + if err != nil { + t.Errorf("startInst(\"i-0c61fb5e573fb0c55\", \"us-east-1\") received %s; Expected %s", err.Error(), c.Expected) + } + + assert.Equal(t, startInst, c.Expected) + + }) + } +} + +func TestEvalInst(t *testing.T) { + //Figure out what hapens with time, and day of the week, and if the instnace starts + + //Input to be used for eval inst. Perhaps pair it down to just the Instances section + goodTime := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("startweekday"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("stopweekday"), + Value: aws.String("17:00"), + }, + }, + }, + }, + }, + }, + } + + badTime := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("startweekday"), + Value: aws.String("155540:0"), + }, + { + Key: aws.String("stopweekday"), + Value: aws.String("17:00"), + }, + }, + }, + }, + }, + }, + } + + //badTag meaning autoscaling group to be found + badTag := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("startweekday"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("stopweekday"), + Value: aws.String("17:00"), + }, + { + Key: aws.String("aws:autoscaling:groupName"), + Value: aws.String("testing-autoscaling-group"), + }, + }, + }, + }, + }, + }, + } + + cases := []struct { + Name string + Expected string + Time time.Time + Input *ec2.Instance + }{ + { + Name: "StartInstance", + Expected: "Starting Instance: i-0c938b5e573fb0f26", + Time: time.Date(2000, 11, 17, 15, 02, 00, 0, time.UTC), + Input: goodTime.Reservations[0].Instances[0], + }, + { + Name: "Start time mismatch", + Expected: "StartWeekDay schedule not matched for: i-0c938b5e573fb0f26", + Time: time.Date(2000, 11, 17, 22, 02, 00, 0, time.UTC), + Input: goodTime.Reservations[0].Instances[0], + }, + { + Name: "Weekend not Weekday", + Expected: "Current day of week Saturday. StartWeekDay requires non weekend values", + Time: time.Date(2000, 11, 18, 22, 02, 00, 0, time.UTC), + Input: goodTime.Reservations[0].Instances[0], + }, + { + Name: "Improper tag time format", + Expected: "", //Error will throw instead + Time: time.Date(2000, 11, 17, 15, 02, 00, 0, time.UTC), + Input: badTime.Reservations[0].Instances[0], + }, + { + Name: "Autoscaling group present", + Expected: "Skipping - i-0c938b5e573fb0f26 is part of autoscaling group testing-autoscaling-group", + Time: time.Date(2000, 11, 17, 15, 02, 00, 0, time.UTC), + Input: badTag.Reservations[0].Instances[0], + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + //Put in the rest of the code to pass the mocked Start Inst + e := mockStartInst{} + result, err := evalInst(c.Input, "us-east-1", c.Time, e) + if err != nil { + assert.Equal(t, "parsing time \"155540:0\" as \"15:04\": cannot parse \"5540:0\" as \":\"", err.Error()) + } + assert.Equal(t, c.Expected, result) + }) + } +} diff --git a/golang_auto_start_stop_ec2/ec2sched/startWeekEnd/main.go b/golang_auto_start_stop_ec2/ec2sched/startWeekEnd/main.go new file mode 100644 index 0000000..cd99220 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/startWeekEnd/main.go @@ -0,0 +1,193 @@ +package main + +import ( + "fmt" + "go/ec2sched/pkg/sess" + "go/ec2sched/pkg/settz" + "os" + "time" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" +) + +type startInst interface { + startInst(instId string, region string) (string, error) +} + +type instInfo interface { + instInfo(tagName string, region string) (*ec2.DescribeInstancesOutput, error) +} + +type ec2Api struct { + Client ec2iface.EC2API +} + +func (e ec2Api) instInfo(tagName string, region string) (*ec2.DescribeInstancesOutput, error) { + + var maxOutput int = 75 + m := int64(maxOutput) + var resp *ec2.DescribeInstancesOutput + + input := &ec2.DescribeInstancesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("tag-key"), + Values: []*string{ + aws.String(tagName), + }, + }, + }, + MaxResults: &m, + } + + //Cycle through paginated results for describe instances (incase we have more than 75 instances) + for { + instOutput, err := e.Client.DescribeInstances(input) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + return nil, awsErr + } + return nil, err + } + + if resp == nil { + resp = instOutput + } else { + resp.Reservations = append(resp.Reservations, instOutput.Reservations...) + } + + if instOutput.NextToken == nil { + break + } + + input.NextToken = instOutput.NextToken + } + //fmt.Println(resp) + return resp, nil +} + +func (e ec2Api) startInst(instId string, region string) (string, error) { + input := &ec2.StartInstancesInput{ + InstanceIds: []*string{ + aws.String(instId), + }, + } + + res, si_err := e.Client.StartInstances(input) + if si_err != nil { + if awsErr, ok := si_err.(awserr.Error); ok { + fmt.Println(awsErr.Error()) + return "", awsErr + } else { + fmt.Println(si_err.Error()) + return "", si_err + } + } + //fmt.Println(res) + return *res.StartingInstances[0].InstanceId, nil +} + +func evalInst(inst *ec2.Instance, region string, curTime time.Time, s startInst) (string, error) { + dayOfWeek := curTime.Weekday() + modTime := curTime.Format(("15:04")) + + //startTime is the desired start time for the ec2 instance + startTime := "" + + fmt.Println(*inst.InstanceId) + //Checks to see if weekday. If so skip out + if int(dayOfWeek) < 6 || int(dayOfWeek) > 7 { + return fmt.Sprintf("Current day of week %s. StartWeekEnd requires non weekday values", dayOfWeek), nil + } + + for _, tag := range inst.Tags { + if *tag.Key == "StartWeekEnd" { + //fmt.Println("StartWeekEnd tag found. Value: ", *tag.Value) + startTime = *tag.Value + } + //ASGs not supported + if *tag.Key == "aws:autoscaling:groupName" { + return fmt.Sprintf("Skipping - %s is part of autoscaling group %s", string(*inst.InstanceId), *tag.Value), nil + } + } + + //Point here is to get the times on the same day to compare time of day, not just the date. Ensuring Apples to Apples + cur_tod, _ := time.Parse("15:04", modTime) + start_tod, start_tod_err := time.Parse("15:04", startTime) + + if start_tod_err != nil { + return "", start_tod_err + } + + cur_minus := cur_tod.Add(-time.Minute * 5) + cur_plus := cur_tod.Add(time.Minute * 5) + + if start_tod.After(cur_minus) && start_tod.Before(cur_plus) { + result, err := s.startInst(*inst.InstanceId, region) + if err != nil { + return "", err + } + return "Starting Instance: " + result, nil + } else { + strVal := string(*inst.InstanceId) + return "StartWeekEnd schedule not matched for: " + strVal, nil + } +} + +// Handler doing bulk of work +// Environment variables set at the lambda configuration level +func HandleLambdaEvent() { + + //Tag to look for on the instance + schedule_tag := "startweekend" + tz, err := settz.SetRegion(os.Getenv("TZ")) + if err != nil { + fmt.Println(err.Error()) + } + fmt.Println(tz) + fmt.Println(time.Now()) + + region := os.Getenv("REGION_TZ") + + sess, err := sess.EstablishSession(region) + e := ec2Api{ + Client: ec2.New(sess), + } + + instDesc, err := e.instInfo(schedule_tag, region) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + instCount := 0 + + for idx, res := range instDesc.Reservations { + instCount += len(res.Instances) + + for _, inst := range instDesc.Reservations[idx].Instances { + if *inst.State.Name != "running" { + st, err := evalInst(inst, region, time.Now(), e) + if err != nil { + fmt.Println(err.Error()) + } + fmt.Println(st) + + } else { + fmt.Printf("Instance: %s is already running\n", *inst.InstanceId) + } + } + fmt.Println("-----") + } + fmt.Printf("Instance count evaluated with %s tag: %d", schedule_tag, instCount) + return +} + +// Go must call main function first, so we call the handler from the main. +func main() { + lambda.Start(HandleLambdaEvent) +} diff --git a/golang_auto_start_stop_ec2/ec2sched/startWeekEnd/main_test.go b/golang_auto_start_stop_ec2/ec2sched/startWeekEnd/main_test.go new file mode 100644 index 0000000..ff55158 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/startWeekEnd/main_test.go @@ -0,0 +1,308 @@ +package main + +import ( + "fmt" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/stretchr/testify/assert" +) + +// Create a mock structure implementing the ec2iface interface. Makes the +// call to aws ec2 api +type MockEC2Client struct { + ec2iface.EC2API + DescribeInstancesOutputValue ec2.DescribeInstancesOutput + StartInstancesOutputValue ec2.StartInstancesOutput +} + +// Implements the interface +type mockStartInst struct{} + +// Implement the mocked DescribeInstances function +func (m *MockEC2Client) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) { + return &m.DescribeInstancesOutputValue, nil +} + +func (m *MockEC2Client) StartInstances(input *ec2.StartInstancesInput) (*ec2.StartInstancesOutput, error) { + return &m.StartInstancesOutputValue, nil +} + +func (m mockStartInst) startInst(instId string, region string) (string, error) { + return "i-0c938b5e573fb0f26", nil +} + +func TestInstInfo(t *testing.T) { + //Becauase we use the filter for included in the DescribeInstances we're testing to see + //we get a valid data type back + + //Response from ec2 aws api and the expected result for function instInfo + descInstancesWithStartWeekEnd := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("StartWeekEnd"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("StopWeekEnd"), + Value: aws.String("17:00"), + }, + }, + }, + }, + }, + }, + } + + cases := []struct { + Name string + Resp ec2.DescribeInstancesOutput + Expected ec2.DescribeInstancesOutput + }{ + { + Name: "StartWeekEndPresent", + Resp: descInstancesWithStartWeekEnd, + Expected: descInstancesWithStartWeekEnd, + }, + } + + //time to iterate over the test cases + for _, c := range cases { + + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + e := ec2Api{ + Client: &MockEC2Client{ + EC2API: nil, + DescribeInstancesOutputValue: c.Resp, + }, + } + inst, err := e.instInfo("StartWeekEnd", "us-east-1") + + if err != nil { + fmt.Println("Unexpected Error - ", err.Error()) + } + + assert.Equal(t, "*ec2.DescribeInstancesOutput", fmt.Sprintf("%T", inst)) + + }) + } +} + +func TestStartInst(t *testing.T) { + //Do we start the instance? What do we get back from the startInst function + + //The outputs we're mocking for the ec2 aws api for StartInstances + var ( + //Output when starting Instance + startInstancesOutput = ec2.StartInstancesOutput{ + StartingInstances: []*ec2.InstanceStateChange{ + { + InstanceId: aws.String("i-0c61fb5e573fb0c55"), + PreviousState: &ec2.InstanceState{ + Name: aws.String("stopped"), + }, + }, + }, + } + + //Output when instance is stopped + startInstancesOutputStopped = ec2.StartInstancesOutput{ + StartingInstances: []*ec2.InstanceStateChange{ + { + InstanceId: aws.String("i-0c61fb5e573fb0c55"), + PreviousState: &ec2.InstanceState{ + Name: aws.String("running"), + }, + }, + }, + } + ) + + cases := []struct { + Name string + Resp ec2.StartInstancesOutput + Expected string + }{ + { + Name: "ProperInstanceId", + Resp: startInstancesOutput, + Expected: "i-0c61fb5e573fb0c55", + }, + { + Name: "InstanceRunning", + Resp: startInstancesOutputStopped, + Expected: "i-0c61fb5e573fb0c55", + }, + } + + for _, c := range cases { + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + e := ec2Api{ + Client: &MockEC2Client{ + EC2API: nil, + StartInstancesOutputValue: c.Resp, + }, + } + startInst, err := e.startInst("i-0c61fb5e573fb0c55", "us-east-1") + + if err != nil { + t.Errorf("startInst(\"i-0c61fb5e573fb0c55\", \"us-east-1\") received %s; Expected %s", err.Error(), c.Expected) + } + + assert.Equal(t, startInst, c.Expected) + + }) + } +} + +func TestEvalInst(t *testing.T) { + //Figure out what hapens with time, and day of the week, and if the instnace starts + + //Input to be used for eval inst. Perhaps pair it down to just the Instances section + goodTime := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("StartWeekEnd"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("StopWeekEnd"), + Value: aws.String("17:00"), + }, + }, + }, + }, + }, + }, + } + + badTime := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("StartWeekEnd"), + Value: aws.String("155540:0"), + }, + { + Key: aws.String("StopWeekEnd"), + Value: aws.String("17:00"), + }, + }, + }, + }, + }, + }, + } + + //badTag meaning autoscaling group to be found + badTag := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("StartWeekEnd"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("StopWeekEnd"), + Value: aws.String("17:00"), + }, + { + Key: aws.String("aws:autoscaling:groupName"), + Value: aws.String("testing-autoscaling-group"), + }, + }, + }, + }, + }, + }, + } + + cases := []struct { + Name string + Expected string + Time time.Time + Input *ec2.Instance + }{ + { + Name: "StartInstance", + Expected: "Starting Instance: i-0c938b5e573fb0f26", + Time: time.Date(2000, 11, 18, 15, 02, 00, 0, time.UTC), + Input: goodTime.Reservations[0].Instances[0], + }, + { + Name: "Start time mismatch", + Expected: "StartWeekEnd schedule not matched for: i-0c938b5e573fb0f26", + Time: time.Date(2000, 11, 18, 22, 02, 00, 0, time.UTC), + Input: goodTime.Reservations[0].Instances[0], + }, + { + Name: "Weekday not Weekend", + Expected: "Current day of week Friday. StartWeekEnd requires non weekday values", + Time: time.Date(2000, 11, 17, 22, 02, 00, 0, time.UTC), + Input: goodTime.Reservations[0].Instances[0], + }, + { + Name: "Improper tag time format", + Expected: "", //Error will throw instead + Time: time.Date(2000, 11, 18, 15, 02, 00, 0, time.UTC), + Input: badTime.Reservations[0].Instances[0], + }, + { + Name: "Autoscaling group present", + Expected: "Skipping - i-0c938b5e573fb0f26 is part of autoscaling group testing-autoscaling-group", + Time: time.Date(2000, 11, 18, 15, 02, 00, 0, time.UTC), + Input: badTag.Reservations[0].Instances[0], + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + //Put in the rest of the code to pass the mocked Start Inst + e := mockStartInst{} + result, err := evalInst(c.Input, "us-east-1", c.Time, e) + if err != nil { + assert.Equal(t, "parsing time \"155540:0\" as \"15:04\": cannot parse \"5540:0\" as \":\"", err.Error()) + } + assert.Equal(t, c.Expected, result) + }) + } +} diff --git a/golang_auto_start_stop_ec2/ec2sched/stopWeekDay/main.go b/golang_auto_start_stop_ec2/ec2sched/stopWeekDay/main.go new file mode 100644 index 0000000..b6633fb --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/stopWeekDay/main.go @@ -0,0 +1,194 @@ +package main + +import ( + "fmt" + "go/ec2sched/pkg/sess" + "go/ec2sched/pkg/settz" + "os" + "time" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" +) + +type stoptInst interface { + stopInst(instId string, region string) (string, error) +} + +type instInfo interface { + instInfo(tagName string, region string) (*ec2.DescribeInstancesOutput, error) +} + +type ec2Api struct { + Client ec2iface.EC2API +} + +func (e ec2Api) instInfo(tagName string, region string) (*ec2.DescribeInstancesOutput, error) { + var maxOutput int = 75 + m := int64(maxOutput) + var resp *ec2.DescribeInstancesOutput + + input := &ec2.DescribeInstancesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("tag-key"), + Values: []*string{ + aws.String(tagName), + }, + }, + }, + MaxResults: &m, + } + + //Cycle through paginated results for describe instances (incase we have more than 75 instances) + for { + instOutput, err := e.Client.DescribeInstances(input) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + return nil, awsErr + } + return nil, err + } + + if resp == nil { + resp = instOutput + } else { + resp.Reservations = append(resp.Reservations, instOutput.Reservations...) + } + + if instOutput.NextToken == nil { + break + } + + input.NextToken = instOutput.NextToken + } + //fmt.Println(resp) + return resp, nil +} + +func (e ec2Api) stopInst(instId string, region string) (string, error) { + + input := &ec2.StopInstancesInput{ + InstanceIds: []*string{ + aws.String(instId), + }, + } + + res, si_err := e.Client.StopInstances(input) + if si_err != nil { + if awsErr, ok := si_err.(awserr.Error); ok { + fmt.Println(awsErr.Error()) + return "", awsErr + } else { + fmt.Println(si_err.Error()) + return "", si_err + } + } + //fmt.Println(res) + return *res.StoppingInstances[0].InstanceId, nil +} + +func evalInst(inst *ec2.Instance, region string, curTime time.Time, s stoptInst) (string, error) { + dayOfWeek := curTime.Weekday() + modTime := curTime.Format(("15:04")) + + //stopTime is the desired stop time for the ec2 instance + stopTime := "" + + fmt.Println(*inst.InstanceId) + + //Check to see if weekend + if int(dayOfWeek) >= 6 && int(dayOfWeek) <= 7 { + return fmt.Sprintf("Current day of week %s. StopWeekDay requires non weekend values", dayOfWeek), nil + } + + for _, tag := range inst.Tags { + if *tag.Key == "StopWeekDay" { + //fmt.Println("StopWeekDay tag found. Value: ", *tag.Value) + stopTime = *tag.Value + } + //ASGs not supported + if *tag.Key == "aws:autoscaling:groupName" { + return fmt.Sprintf("Skipping - %s is part of autoscaling group %s", string(*inst.InstanceId), *tag.Value), nil + } + } + + //Point here is to get the times on the same day to compare time of day, not just the date. Ensuring Apples to Apples + cur_tod, _ := time.Parse("15:04", modTime) + stop_tod, stop_tod_err := time.Parse("15:04", stopTime) + + if stop_tod_err != nil { + return "", stop_tod_err + } + + cur_minus := cur_tod.Add(-time.Minute * 5) + cur_plus := cur_tod.Add(time.Minute * 5) + + if stop_tod.After(cur_minus) && stop_tod.Before(cur_plus) { + result, err := s.stopInst(*inst.InstanceId, region) + if err != nil { + return "", err + } + return "Stopping Instance: " + result, nil + } else { + strVal := string(*inst.InstanceId) + return "StopWeekDay schedule not matched for: " + strVal, nil + } +} + +// Handler doing bulk of work +// Environment variables set at the lambda configuration level +func HandleLambdaEvent() { + + //Tag to look for on the instances + schedule_tag := "stopweekday" + tz, err := settz.SetRegion(os.Getenv("TZ")) + if err != nil { + fmt.Println(err.Error()) + } + fmt.Println(tz) + fmt.Println(time.Now()) + + region := os.Getenv("REGION_TZ") + + sess, err := sess.EstablishSession(region) + e := ec2Api{ + Client: ec2.New(sess), + } + + instDesc, err := e.instInfo(schedule_tag, region) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + instCount := 0 + + for idx, res := range instDesc.Reservations { + instCount += len(res.Instances) + + for _, inst := range instDesc.Reservations[idx].Instances { + if *inst.State.Name != "stopped" { + st, err := evalInst(inst, region, time.Now(), e) + if err != nil { + fmt.Println(err.Error()) + } + fmt.Println(st) + + } else { + fmt.Printf("Instance: %s is already stopped\n", *inst.InstanceId) + } + } + fmt.Println("-----") + } + fmt.Printf("Instance count evaluated with %s tag: %d", schedule_tag, instCount) + return +} + +// Go must call main function first, so we call the handler from the main. +func main() { + lambda.Start(HandleLambdaEvent) +} diff --git a/golang_auto_start_stop_ec2/ec2sched/stopWeekDay/main_test.go b/golang_auto_start_stop_ec2/ec2sched/stopWeekDay/main_test.go new file mode 100644 index 0000000..73ccb90 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/stopWeekDay/main_test.go @@ -0,0 +1,307 @@ +package main + +import ( + "fmt" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/stretchr/testify/assert" +) + +// Create a mock structure implementing the ec2iface interface. Makes the +// call to aws ec2 api +type MockEC2Client struct { + ec2iface.EC2API + DescribeInstancesOutputValue ec2.DescribeInstancesOutput + StopInstancesOutputValue ec2.StopInstancesOutput +} + +// Implements the interface +type mockStopInst struct{} + +// Implement the mocked DescribeInstances function +func (m *MockEC2Client) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) { + return &m.DescribeInstancesOutputValue, nil +} + +func (m *MockEC2Client) StopInstances(input *ec2.StopInstancesInput) (*ec2.StopInstancesOutput, error) { + return &m.StopInstancesOutputValue, nil +} + +func (m mockStopInst) stopInst(instId string, region string) (string, error) { + return "i-0c938b5e573fb0f26", nil +} + +func TestInstInfo(t *testing.T) { + //Becauase we use the filter for included in the DescribeInstances we're testing to see + //we get a valid data type back + + //Response from ec2 aws api and the expected result for function instInfo + descInstancesWithStopWeekDay := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("StartWeekDay"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("StopWeekDay"), + Value: aws.String("17:00"), + }, + }, + }, + }, + }, + }, + } + + cases := []struct { + Name string + Resp ec2.DescribeInstancesOutput + Expected ec2.DescribeInstancesOutput + }{ + { + Name: "StopWeekDayPresent", + Resp: descInstancesWithStopWeekDay, + Expected: descInstancesWithStopWeekDay, + }, + } + + //time to iterate over the test cases + for _, c := range cases { + + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + e := ec2Api{ + Client: &MockEC2Client{ + EC2API: nil, + DescribeInstancesOutputValue: c.Resp, + }, + } + inst, err := e.instInfo("StopWeekDay", "us-east-1") + + if err != nil { + fmt.Println("Unexpected Error - ", err.Error()) + } + + assert.Equal(t, "*ec2.DescribeInstancesOutput", fmt.Sprintf("%T", inst)) + + }) + } +} + +func TestStopInst(t *testing.T) { + //Do we stop the instance? What do we get back from the startInst function + + //The outputs we're mocking for the ec2 aws api for StartInstances + var ( + //Output when stopping Instance + stopInstancesOutput = ec2.StopInstancesOutput{ + StoppingInstances: []*ec2.InstanceStateChange{ + { + InstanceId: aws.String("i-0c61fb5e573fb0c55"), + PreviousState: &ec2.InstanceState{ + Name: aws.String("running"), + }, + }, + }, + } + + //Output when instance is already stopped + stopInstancesOutputStopped = ec2.StopInstancesOutput{ + StoppingInstances: []*ec2.InstanceStateChange{ + { + InstanceId: aws.String("i-0c61fb5e573fb0c55"), + PreviousState: &ec2.InstanceState{ + Name: aws.String("stopped"), + }, + }, + }, + } + ) + + cases := []struct { + Name string + Resp ec2.StopInstancesOutput + Expected string + }{ + { + Name: "ProperInstanceId", + Resp: stopInstancesOutput, + Expected: "i-0c61fb5e573fb0c55", + }, + { + Name: "InstanceRunning", + Resp: stopInstancesOutputStopped, + Expected: "i-0c61fb5e573fb0c55", + }, + } + + for _, c := range cases { + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + e := ec2Api{ + Client: &MockEC2Client{ + EC2API: nil, + StopInstancesOutputValue: c.Resp, + }, + } + stopInst, err := e.stopInst("i-0c61fb5e573fb0c55", "us-east-1") + + if err != nil { + t.Errorf("stopInst(\"i-0c61fb5e573fb0c55\", \"us-east-1\") received %s; Expected %s", err.Error(), c.Expected) + } + + assert.Equal(t, stopInst, c.Expected) + + }) + } +} + +func TestEvalInst(t *testing.T) { + //Figure out what hapens with time, and day of the week, and if the instnace starts + + //Input to be used for eval inst. Perhaps pair it down to just the Instances section + goodTime := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("StartWeekDay"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("StopWeekDay"), + Value: aws.String("17:00"), + }, + }, + }, + }, + }, + }, + } + + badTime := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("StartWeekDay"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("StopWeekDay"), + Value: aws.String("155540:0"), + }, + }, + }, + }, + }, + }, + } + + badTag := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("StartWeekDay"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("StopWeekDay"), + Value: aws.String("17:00"), + }, + { + Key: aws.String("aws:autoscaling:groupName"), + Value: aws.String("testing-autoscaling-group"), + }, + }, + }, + }, + }, + }, + } + + cases := []struct { + Name string + Expected string + Time time.Time + Input *ec2.Instance + }{ + { + Name: "StopInstance", + Expected: "Stopping Instance: i-0c938b5e573fb0f26", + Time: time.Date(2000, 11, 17, 17, 02, 00, 0, time.UTC), + Input: goodTime.Reservations[0].Instances[0], + }, + { + Name: "Stop time mismatch", + Expected: "StopWeekDay schedule not matched for: i-0c938b5e573fb0f26", + Time: time.Date(2000, 11, 17, 22, 02, 00, 0, time.UTC), + Input: goodTime.Reservations[0].Instances[0], + }, + { + Name: "Weekend not Weekday", + Expected: "Current day of week Saturday. StopWeekDay requires non weekend values", + Time: time.Date(2000, 11, 18, 22, 02, 00, 0, time.UTC), + Input: goodTime.Reservations[0].Instances[0], + }, + { + Name: "Improper tag time format", + Expected: "", //Error will throw instead + Time: time.Date(2000, 11, 17, 15, 02, 00, 0, time.UTC), + Input: badTime.Reservations[0].Instances[0], + }, + { + Name: "Autoscaling group present", + Expected: "Skipping - i-0c938b5e573fb0f26 is part of autoscaling group testing-autoscaling-group", + Time: time.Date(2000, 11, 17, 17, 02, 00, 0, time.UTC), + Input: badTag.Reservations[0].Instances[0], + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + //Put in the rest of the code to pass the mocked Start Inst + e := mockStopInst{} + result, err := evalInst(c.Input, "us-east-1", c.Time, e) + if err != nil { + assert.Equal(t, "parsing time \"155540:0\" as \"15:04\": cannot parse \"5540:0\" as \":\"", err.Error()) + } + assert.Equal(t, c.Expected, result) + }) + } +} diff --git a/golang_auto_start_stop_ec2/ec2sched/stopWeekEnd/main.go b/golang_auto_start_stop_ec2/ec2sched/stopWeekEnd/main.go new file mode 100644 index 0000000..61cc14a --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/stopWeekEnd/main.go @@ -0,0 +1,195 @@ +package main + +import ( + "fmt" + "go/ec2sched/pkg/sess" + "go/ec2sched/pkg/settz" + "os" + "time" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" +) + +type stoptInst interface { + stopInst(instId string, region string) (string, error) +} + +type instInfo interface { + instInfo(tagName string, region string) (*ec2.DescribeInstancesOutput, error) +} + +type ec2Api struct { + Client ec2iface.EC2API +} + +func (e ec2Api) instInfo(tagName string, region string) (*ec2.DescribeInstancesOutput, error) { + + var maxOutput int = 75 + m := int64(maxOutput) + var resp *ec2.DescribeInstancesOutput + + input := &ec2.DescribeInstancesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("tag-key"), + Values: []*string{ + aws.String(tagName), + }, + }, + }, + MaxResults: &m, + } + + //Cycle through paginated results for describe instances (incase we have more than 75 instances) + for { + instOutput, err := e.Client.DescribeInstances(input) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + return nil, awsErr + } + return nil, err + } + + if resp == nil { + resp = instOutput + } else { + resp.Reservations = append(resp.Reservations, instOutput.Reservations...) + } + + if instOutput.NextToken == nil { + break + } + + input.NextToken = instOutput.NextToken + } + //fmt.Println(resp) + return resp, nil +} + +func (e ec2Api) stopInst(instId string, region string) (string, error) { + + input := &ec2.StopInstancesInput{ + InstanceIds: []*string{ + aws.String(instId), + }, + } + + res, si_err := e.Client.StopInstances(input) + if si_err != nil { + if awsErr, ok := si_err.(awserr.Error); ok { + fmt.Println(awsErr.Error()) + return "", awsErr + } else { + fmt.Println(si_err.Error()) + return "", si_err + } + } + //fmt.Println(res) + return *res.StoppingInstances[0].InstanceId, nil +} + +func evalInst(inst *ec2.Instance, region string, curTime time.Time, s stoptInst) (string, error) { + dayOfWeek := curTime.Weekday() + modTime := curTime.Format(("15:04")) + + //stopTime is the desired stop time for the ec2 instance + stopTime := "" + + fmt.Println(*inst.InstanceId) + + //Check to see if weekend + if int(dayOfWeek) < 6 || int(dayOfWeek) > 7 { + return fmt.Sprintf("Current day of week %s. StopWeekEnd requires non weekday values", dayOfWeek), nil + } + + for _, tag := range inst.Tags { + if *tag.Key == "StopWeekEnd" { + //fmt.Println("StopWeekEnd tag found. Value: ", *tag.Value) + stopTime = *tag.Value + } + //ASGs not supported + if *tag.Key == "aws:autoscaling:groupName" { + return fmt.Sprintf("Skipping - %s is part of autoscaling group %s", string(*inst.InstanceId), *tag.Value), nil + } + } + + //Point here is to get the times on the same day to compare time of day, not just the date. Ensuring Apples to Apples + cur_tod, _ := time.Parse("15:04", modTime) + stop_tod, stop_tod_err := time.Parse("15:04", stopTime) + + if stop_tod_err != nil { + return "", stop_tod_err + } + + cur_minus := cur_tod.Add(-time.Minute * 5) + cur_plus := cur_tod.Add(time.Minute * 5) + + if stop_tod.After(cur_minus) && stop_tod.Before(cur_plus) { + result, err := s.stopInst(*inst.InstanceId, region) + if err != nil { + return "", err + } + return "Stopping Instance: " + result, nil + } else { + strVal := string(*inst.InstanceId) + return "StopWeekEnd schedule not matched for: " + strVal, nil + } +} + +// Handler doing bulk of work +// Environment variables set at the lambda configuration level +func HandleLambdaEvent() { + + //Tag to look for on the instances + schedule_tag := "stopweekend" + tz, err := settz.SetRegion(os.Getenv("TZ")) + if err != nil { + fmt.Println(err.Error()) + } + fmt.Println(tz) + fmt.Println(time.Now()) + + region := os.Getenv("REIGON_TZ") + + sess, err := sess.EstablishSession(region) + e := ec2Api{ + Client: ec2.New(sess), + } + + instDesc, err := e.instInfo(schedule_tag, region) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + instCount := 0 + + for idx, res := range instDesc.Reservations { + instCount += len(res.Instances) + + for _, inst := range instDesc.Reservations[idx].Instances { + if *inst.State.Name != "stopped" { + st, err := evalInst(inst, region, time.Now(), e) + if err != nil { + fmt.Println(err.Error()) + } + fmt.Println(st) + + } else { + fmt.Printf("Instance: %s is already stopped\n", *inst.InstanceId) + } + } + fmt.Println("-----") + } + fmt.Printf("Instance count evaluated with %s tag: %d", schedule_tag, instCount) + return +} + +// Go must call main function first, so we call the handler from the main. +func main() { + lambda.Start(HandleLambdaEvent) +} diff --git a/golang_auto_start_stop_ec2/ec2sched/stopWeekEnd/main_test.go b/golang_auto_start_stop_ec2/ec2sched/stopWeekEnd/main_test.go new file mode 100644 index 0000000..e8b88f4 --- /dev/null +++ b/golang_auto_start_stop_ec2/ec2sched/stopWeekEnd/main_test.go @@ -0,0 +1,307 @@ +package main + +import ( + "fmt" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/stretchr/testify/assert" +) + +// Create a mock structure implementing the ec2iface interface. Makes the +// call to aws ec2 api +type MockEC2Client struct { + ec2iface.EC2API + DescribeInstancesOutputValue ec2.DescribeInstancesOutput + StopInstancesOutputValue ec2.StopInstancesOutput +} + +// Implements the interface +type mockStopInst struct{} + +// Implement the mocked DescribeInstances function +func (m *MockEC2Client) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) { + return &m.DescribeInstancesOutputValue, nil +} + +func (m *MockEC2Client) StopInstances(input *ec2.StopInstancesInput) (*ec2.StopInstancesOutput, error) { + return &m.StopInstancesOutputValue, nil +} + +func (m mockStopInst) stopInst(instId string, region string) (string, error) { + return "i-0c938b5e573fb0f26", nil +} + +func TestInstInfo(t *testing.T) { + //Becauase we use the filter for included in the DescribeInstances we're testing to see + //we get a valid data type back + + //Response from ec2 aws api and the expected result for function instInfo + descInstancesWithStopWeekEnd := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("StartWeekEnd"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("StopWeekEnd"), + Value: aws.String("17:00"), + }, + }, + }, + }, + }, + }, + } + + cases := []struct { + Name string + Resp ec2.DescribeInstancesOutput + Expected ec2.DescribeInstancesOutput + }{ + { + Name: "StopWeekEndPresent", + Resp: descInstancesWithStopWeekEnd, + Expected: descInstancesWithStopWeekEnd, + }, + } + + //time to iterate over the test cases + for _, c := range cases { + + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + e := ec2Api{ + Client: &MockEC2Client{ + EC2API: nil, + DescribeInstancesOutputValue: c.Resp, + }, + } + inst, err := e.instInfo("StopWeekEnd", "us-east-1") + + if err != nil { + fmt.Println("Unexpected Error - ", err.Error()) + } + + assert.Equal(t, "*ec2.DescribeInstancesOutput", fmt.Sprintf("%T", inst)) + + }) + } +} + +func TestStopInst(t *testing.T) { + //Do we stop the instance? What do we get back from the startInst function + + //The outputs we're mocking for the ec2 aws api for StartInstances + var ( + //Output when stopping Instance + stopInstancesOutput = ec2.StopInstancesOutput{ + StoppingInstances: []*ec2.InstanceStateChange{ + { + InstanceId: aws.String("i-0c61fb5e573fb0c55"), + PreviousState: &ec2.InstanceState{ + Name: aws.String("running"), + }, + }, + }, + } + + //Output when instance is already stopped + stopInstancesOutputStopped = ec2.StopInstancesOutput{ + StoppingInstances: []*ec2.InstanceStateChange{ + { + InstanceId: aws.String("i-0c61fb5e573fb0c55"), + PreviousState: &ec2.InstanceState{ + Name: aws.String("stopped"), + }, + }, + }, + } + ) + + cases := []struct { + Name string + Resp ec2.StopInstancesOutput + Expected string + }{ + { + Name: "ProperInstanceId", + Resp: stopInstancesOutput, + Expected: "i-0c61fb5e573fb0c55", + }, + { + Name: "InstanceRunning", + Resp: stopInstancesOutputStopped, + Expected: "i-0c61fb5e573fb0c55", + }, + } + + for _, c := range cases { + //Sub test for each case + t.Run(c.Name, func(t *testing.T) { + e := ec2Api{ + Client: &MockEC2Client{ + EC2API: nil, + StopInstancesOutputValue: c.Resp, + }, + } + stopInst, err := e.stopInst("i-0c61fb5e573fb0c55", "us-east-1") + + if err != nil { + t.Errorf("stopInst(\"i-0c61fb5e573fb0c55\", \"us-east-1\") received %s; Expected %s", err.Error(), c.Expected) + } + + assert.Equal(t, stopInst, c.Expected) + + }) + } +} + +func TestEvalInst(t *testing.T) { + //Figure out what hapens with time, and day of the week, and if the instnace starts + + //Input to be used for eval inst. Perhaps pair it down to just the Instances section + goodTime := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("StartWeekEnd"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("StopWeekEnd"), + Value: aws.String("17:00"), + }, + }, + }, + }, + }, + }, + } + + badTime := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("StartWeekEnd"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("StopWeekEnd"), + Value: aws.String("155540:0"), + }, + }, + }, + }, + }, + }, + } + + badTag := ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-0c938b5e573fb0f26"), + InstanceType: aws.String("m5.large"), + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameStopped), + }, + Tags: []*ec2.Tag{ + { + Key: aws.String("StartWeekEnd"), + Value: aws.String("15:00"), + }, + { + Key: aws.String("StopWeekEnd"), + Value: aws.String("17:00"), + }, + { + Key: aws.String("aws:autoscaling:groupName"), + Value: aws.String("testing-autoscaling-group"), + }, + }, + }, + }, + }, + }, + } + + cases := []struct { + Name string + Expected string + Time time.Time + Input *ec2.Instance + }{ + { + Name: "StopInstance", + Expected: "Stopping Instance: i-0c938b5e573fb0f26", + Time: time.Date(2000, 11, 18, 17, 02, 00, 0, time.UTC), + Input: goodTime.Reservations[0].Instances[0], + }, + { + Name: "Stop time mismatch", + Expected: "StopWeekEnd schedule not matched for: i-0c938b5e573fb0f26", + Time: time.Date(2000, 11, 18, 22, 02, 00, 0, time.UTC), + Input: goodTime.Reservations[0].Instances[0], + }, + { + Name: "Weekday not Weekend", + Expected: "Current day of week Friday. StopWeekEnd requires non weekday values", + Time: time.Date(2000, 11, 17, 22, 02, 00, 0, time.UTC), + Input: goodTime.Reservations[0].Instances[0], + }, + { + Name: "Improper tag time format", + Expected: "", //Error will throw instead + Time: time.Date(2000, 11, 18, 15, 02, 00, 0, time.UTC), + Input: badTime.Reservations[0].Instances[0], + }, + { + Name: "Autoscaling group present", + Expected: "Skipping - i-0c938b5e573fb0f26 is part of autoscaling group testing-autoscaling-group", + Time: time.Date(2000, 11, 18, 17, 02, 00, 0, time.UTC), + Input: badTag.Reservations[0].Instances[0], + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + //Put in the rest of the code to pass the mocked Start Inst + e := mockStopInst{} + result, err := evalInst(c.Input, "us-east-1", c.Time, e) + if err != nil { + assert.Equal(t, "parsing time \"155540:0\" as \"15:04\": cannot parse \"5540:0\" as \":\"", err.Error()) + } + assert.Equal(t, c.Expected, result) + }) + } +}