Skip to content

Commit

Permalink
Improve error handling, additional tests (#24)
Browse files Browse the repository at this point in the history
* fix merge conflicts

* ci: add tests to release action, narrow down tag pattern for test action and run test on all branches

* tests: add terraform for quickly spinning up a test environment

* feat: add execute command test and exit channel, improved error handling

* feat: add terraform for ec2 backed deployments

* fix: remove conditional from err case in goroutine to prevent infinite loop

* fix: remove unecessary return type from Start goroutine
  • Loading branch information
tedsmitt committed Nov 8, 2022
1 parent 95178ae commit 20f7297
Show file tree
Hide file tree
Showing 9 changed files with 616 additions and 57 deletions.
13 changes: 11 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Release
name: Create Release

on:
push:
tags:
- "*"
- "*.*.*"

jobs:
Release:
Expand All @@ -19,6 +19,15 @@ jobs:
with:
go-version: 1.18

- name: Install dependencies
run: |
go mod tidy
# reset go.sum and go.mod so goreleaser won't complain about dirty git state
git checkout HEAD -- go.sum go.mod
- name: Run tests
run: go test -v ./internal

- name: Build
uses: goreleaser/goreleaser-action@v2
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Test
name: Run Tests

on:
push:
branches:
- main
- "**"
pull_request:
branches:
- main
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,13 @@
# vendor/

ecsgo

# For goreleaser otherwise build fails with git dirty error
dist

# Exclude .vscode folder
.vscode/

# Exclude terraform state files from test folder
.terraform*
**.tfstate*
6 changes: 4 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright © 2021 NAME HERE <EMAIL ADDRESS>
Copyright © 2021 Ed Smith ed@edintheclouds.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -15,7 +15,9 @@ limitations under the License.
*/
package main

import cli "github.com/tedsmitt/ecsgo/internal"
import (
cli "github.com/tedsmitt/ecsgo/internal"
)

func main() {
cli.Execute()
Expand Down
33 changes: 17 additions & 16 deletions internal/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"strings"

Expand All @@ -19,7 +18,7 @@ import (
type ExecCommand struct {
input chan string
err chan error
done chan bool
exit chan error
client ecsiface.ECSAPI
region string
endpoint string
Expand All @@ -37,7 +36,7 @@ func CreateExecCommand() *ExecCommand {
e := &ExecCommand{
input: make(chan string, 1),
err: make(chan error, 1),
done: make(chan bool),
exit: make(chan error, 1),
client: client,
region: client.SigningRegion,
endpoint: client.Endpoint,
Expand All @@ -47,12 +46,12 @@ func CreateExecCommand() *ExecCommand {
}

// Start begins a goroutine that listens on the input channel for instructions
func (e *ExecCommand) Start() {
func (e *ExecCommand) Start() error {
// Before we do anything make sure that the session-manager-plugin is available in $PATH, exit if it isn't
_, err := exec.LookPath("session-manager-plugin")
if err != nil {
fmt.Println(red("session-manager-plugin isn't installed or wasn't found in $PATH - https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html"))
os.Exit(1)
return err
}

go func() {
Expand All @@ -74,16 +73,21 @@ func (e *ExecCommand) Start() {
e.getCluster()
}
case err := <-e.err:
fmt.Printf(red("\n%s\n"), err)
os.Exit(1)
e.exit <- err
}
}
}()

// Initiate the workflow
e.input <- "getCluster"
// Block until we receive a message on the done channel
<-e.done

// Block until we receive a message on the exit channel
err = <-e.exit
if err != nil {
return err
}

return nil
}

// Lists available clusters and prompts the user to select one
Expand Down Expand Up @@ -275,6 +279,7 @@ func (e *ExecCommand) executeInput() {
Command: aws.String(command),
Container: e.container.Name,
})

if err != nil {
e.err <- err
return
Expand All @@ -299,15 +304,11 @@ func (e *ExecCommand) executeInput() {

// Print Cluster/Service/Task information to the console
fmt.Printf("\nCluster: %v | Service: %v | Task: %s", cyan(e.cluster), magenta(e.service), green(strings.Split(*e.task.TaskArn, "/")[2]))
fmt.Printf("\nConnecting to container %v", yellow(*e.container.Name))
fmt.Printf("\nConnecting to container %v\n", yellow(*e.container.Name))

// Execute the session-manager-plugin with our task details
if err = runCommand("session-manager-plugin", string(execSess), e.region, "StartSession", "", string(targetJson), e.endpoint); err != nil {
e.done <- true
e.err <- err
return
}
err = runCommand("session-manager-plugin", string(execSess), e.region, "StartSession", "", string(targetJson), e.endpoint)
e.err <- err

e.done <- true
return
}
156 changes: 151 additions & 5 deletions internal/exec_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"fmt"
"os"
"testing"

Expand Down Expand Up @@ -68,7 +69,7 @@ func CreateMockExecCommand(c *MockECSAPI) *ExecCommand {
e := &ExecCommand{
input: make(chan string, 1),
err: make(chan error, 1),
done: make(chan bool),
exit: make(chan error, 1),
client: c,
region: "eu-west-1",
endpoint: "ecs.eu-west-1.amazonaws.com",
Expand Down Expand Up @@ -97,6 +98,19 @@ func TestGetCluster(t *testing.T) {
},
expected: "execCommand",
},
{
name: "TestGetClusterWithSingleResult",
client: &MockECSAPI{
ListClustersMock: func(input *ecs.ListClustersInput) (*ecs.ListClustersOutput, error) {
return &ecs.ListClustersOutput{
ClusterArns: []*string{
aws.String("arn:aws:ecs:eu-west-1:1111111111:cluster/execCommand"),
},
}, nil
},
},
expected: "execCommand",
},
{
name: "TestGetClusterWithoutResults",
client: &MockECSAPI{
Expand All @@ -113,7 +127,10 @@ func TestGetCluster(t *testing.T) {
for _, c := range cases {
input := CreateMockExecCommand(c.client)
input.getCluster()
assert.Equal(t, c.expected, input.cluster)
if ok := assert.Equal(t, c.expected, input.cluster); ok != true {
fmt.Printf("%s FAILED\n", c.name)
}
fmt.Printf("%s PASSED\n", c.name)
}
}

Expand Down Expand Up @@ -154,7 +171,10 @@ func TestGetService(t *testing.T) {
input := CreateMockExecCommand(c.client)
input.cluster = "execCommand"
input.getService()
assert.Equal(t, c.expected, input.service)
if ok := assert.Equal(t, c.expected, input.service); ok != true {
fmt.Printf("%s FAILED\n", c.name)
}
fmt.Printf("%s PASSED\n", c.name)
}
}

Expand Down Expand Up @@ -206,7 +226,10 @@ func TestGetTask(t *testing.T) {
input.cluster = "execCommand"
input.service = "test-service-1"
input.getTask()
assert.Equal(t, c.expected, input.task)
if ok := assert.Equal(t, c.expected, input.task); ok != true {
fmt.Printf("%s FAILED\n", c.name)
}
fmt.Printf("%s PASSED\n", c.name)
}
}

Expand Down Expand Up @@ -254,6 +277,129 @@ func TestGetContainer(t *testing.T) {
input := CreateMockExecCommand(c.client)
input.task = c.task
input.getContainer()
assert.Equal(t, c.expected, input.container)
if ok := assert.Equal(t, c.expected, input.container); ok != true {
fmt.Printf("%s FAILED\n", c.name)
}
fmt.Printf("%s PASSED\n", c.name)
}
}

func TestExecuteInput(t *testing.T) {
cases := []struct {
name string
client *MockECSAPI
expected error
}{
{
name: "TestExecuteInputWithServices",
client: &MockECSAPI{
ListClustersMock: func(input *ecs.ListClustersInput) (*ecs.ListClustersOutput, error) {
return &ecs.ListClustersOutput{
ClusterArns: []*string{
aws.String("arn:aws:ecs:eu-west-1:1111111111:cluster/execCommand"),
},
}, nil
},
ListServicesMock: func(input *ecs.ListServicesInput) (*ecs.ListServicesOutput, error) {
return &ecs.ListServicesOutput{
ServiceArns: []*string{
aws.String("arn:aws:ecs:eu-west-1:1111111111:cluster/execCommand/test-service-1"),
},
}, nil
},
ListTasksMock: func(input *ecs.ListTasksInput) (*ecs.ListTasksOutput, error) {
return &ecs.ListTasksOutput{
TaskArns: []*string{
aws.String("arn:aws:ecs:eu-west-1:111111111111:task/execCommand/8a58117dac38436ba5547e9da5d3ac3d"),
},
}, nil
},
DescribeTasksMock: func(input *ecs.DescribeTasksInput) (*ecs.DescribeTasksOutput, error) {
return &ecs.DescribeTasksOutput{
Tasks: []*ecs.Task{
{
TaskArn: aws.String("arn:aws:ecs:eu-west-1:111111111111:task/execCommand/8a58117dac38436ba5547e9da5d3ac3d"),
Containers: []*ecs.Container{
{
Name: aws.String("nginx"),
RuntimeId: aws.String("544e08d919364be9926186b086c29868-2531612879"),
},
},
PlatformFamily: aws.String("Linux"),
},
},
}, nil
},
ExecuteCommandMock: func(input *ecs.ExecuteCommandInput) (*ecs.ExecuteCommandOutput, error) {
return &ecs.ExecuteCommandOutput{
Session: &ecs.Session{
SessionId: aws.String("ecs-execute-command-0e86561fddf625dc1"),
StreamUrl: aws.String("wss://ssmmessages.eu-west-1.amazonaws.com/v1/data-channel/ecs-execute-command-blah"),
TokenValue: aws.String("abc123"),
},
}, nil
},
},
expected: nil,
},
{
name: "TestExecuteInputWithNoServices",
client: &MockECSAPI{
ListClustersMock: func(input *ecs.ListClustersInput) (*ecs.ListClustersOutput, error) {
return &ecs.ListClustersOutput{
ClusterArns: []*string{
aws.String("arn:aws:ecs:eu-west-1:1111111111:cluster/execCommand"),
},
}, nil
},
ListServicesMock: func(input *ecs.ListServicesInput) (*ecs.ListServicesOutput, error) {
return &ecs.ListServicesOutput{
ServiceArns: []*string{},
}, nil
},
ListTasksMock: func(input *ecs.ListTasksInput) (*ecs.ListTasksOutput, error) {
return &ecs.ListTasksOutput{
TaskArns: []*string{
aws.String("arn:aws:ecs:eu-west-1:111111111111:task/execCommand/8a58117dac38436ba5547e9da5d3ac3d"),
},
}, nil
},
DescribeTasksMock: func(input *ecs.DescribeTasksInput) (*ecs.DescribeTasksOutput, error) {
return &ecs.DescribeTasksOutput{
Tasks: []*ecs.Task{
{
TaskArn: aws.String("arn:aws:ecs:eu-west-1:111111111111:task/execCommand/8a58117dac38436ba5547e9da5d3ac3d"),
Containers: []*ecs.Container{
{
Name: aws.String("nginx"),
RuntimeId: aws.String("544e08d919364be9926186b086c29868-2531612879"),
},
},
PlatformFamily: aws.String("Linux"),
},
},
}, nil
},
ExecuteCommandMock: func(input *ecs.ExecuteCommandInput) (*ecs.ExecuteCommandOutput, error) {
return &ecs.ExecuteCommandOutput{
Session: &ecs.Session{
SessionId: aws.String("ecs-execute-command-0e86561fddf625dc1"),
StreamUrl: aws.String("wss://ssmmessages.eu-west-1.amazonaws.com/v1/data-channel/ecs-execute-command-blah"),
TokenValue: aws.String("abc123"),
},
}, nil
},
},
expected: nil,
},
}

for _, c := range cases {
cmd := CreateMockExecCommand(c.client)
err := cmd.Start()
if ok := assert.Equal(t, c.expected, err); ok != true {
fmt.Printf("%s FAILED\n", c.name)
}
fmt.Printf("%s PASSED\n", c.name)
}
}
Loading

0 comments on commit 20f7297

Please sign in to comment.