Skip to content

Commit

Permalink
refactored bash command
Browse files Browse the repository at this point in the history
  • Loading branch information
ZeljkoBenovic committed Nov 18, 2023
1 parent f21da61 commit 391baae
Show file tree
Hide file tree
Showing 9 changed files with 513 additions and 30 deletions.
30 changes: 30 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package app

import (
"github.com/Trapesys/aws-commander/aws"
"github.com/Trapesys/aws-commander/conf"
"github.com/Trapesys/aws-commander/logger"
"go.uber.org/fx"
"os"
)

func Run() {
fx.New(
fx.Provide(
conf.New,
logger.New,
aws.New,
),
fx.Invoke(mainApp),
fx.NopLogger,
).Run()
}

func mainApp(log logger.Logger, awss aws.Aws) {
if err := awss.Run(); err != nil {
log.Error("Run command error", "err", err)
os.Exit(1)
}

os.Exit(0)
}
81 changes: 81 additions & 0 deletions aws/aws.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package aws

import (
"github.com/Trapesys/aws-commander/aws/ssm"
"github.com/Trapesys/aws-commander/conf"
"github.com/Trapesys/aws-commander/logger"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/pkg/errors"
)

type mode string

const (
bash mode = "bash"
ansible mode = "ansible"
)

type modeHandler func() error

type modesFactory map[mode]modeHandler

var (
ErrModeNotSupported = errors.New("selected mode not supported")
)

type SSM interface {
RunBash() error
RunAnsible() error
}

type Aws struct {
conf conf.Config
ssm SSM
modes modesFactory
}

func New(conf conf.Config, log logger.Logger) Aws {
sess, err := provideSesson(conf)
if err != nil {
log.Fatalln("Could not create AWS session", "err", err.Error())
}

localssm := ssm.New(log, conf, sess)

return Aws{
conf: conf,
ssm: localssm,
modes: modesFactory{
bash: localssm.RunBash,
ansible: localssm.RunAnsible,
},
}
}

func (a *Aws) Run() error {
modeHn, ok := a.modes[mode(a.conf.Mode)]
if !ok {
return ErrModeNotSupported
}

return modeHn()
}

func provideSesson(conf conf.Config) (*session.Session, error) {
sessOpt := session.Options{}
sessConf := aws.Config{}

if conf.AWSRegion != "" {
sessConf.Region = &conf.AWSRegion
}

if conf.AWSProfile != "" {
sessOpt.Profile = conf.AWSProfile
}

sessOpt.Config = sessConf
sessOpt.SharedConfigState = session.SharedConfigEnable

return session.NewSessionWithOptions(sessOpt)
}
165 changes: 165 additions & 0 deletions aws/ssm/ssm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package ssm

import (
"bytes"
"fmt"
"github.com/Trapesys/aws-commander/conf"
"github.com/Trapesys/aws-commander/logger"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/davecgh/go-spew/spew"
"os"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws/session"
assm "github.com/aws/aws-sdk-go/service/ssm"
)

type ssm struct {
log logger.Logger
conf conf.Config

cl *assm.SSM
}

func (s ssm) RunBash() error {
s.log.Info("Running ssm bash command")

command, err := s.cl.SendCommand(&assm.SendCommandInput{
DocumentName: aws.String("AWS-RunShellScript"),
DocumentVersion: aws.String("$LATEST"),
InstanceIds: s.provideInstanceIDs(),
Parameters: s.provideBashCommands(),
TimeoutSeconds: aws.Int64(300),
})
if err != nil {
return err
}

s.log.Info("Command deployed successfully")
s.log.Info("Waiting for results")

var instIdsSuccess = make([]*string, 0)

for _, instId := range command.Command.InstanceIds {
if werr := s.waitForCmdExecutionComplete(command.Command.CommandId, instId); werr != nil {
s.log.Error("Error waiting for command execution", "err", err.Error(), "instance_id", *instId)
} else {
instIdsSuccess = append(instIdsSuccess, instId)
}
}

for _, id := range instIdsSuccess {
out, err := s.cl.GetCommandInvocation(&assm.GetCommandInvocationInput{
CommandId: command.Command.CommandId,
InstanceId: id,
})
if err != nil {
s.log.Error("Could not get command output", "err", "instance_id", *id)
} else {
displayResults(id, out)
}
}

return nil
}

func (s ssm) RunAnsible() error {
s.log.Info("Running ssm ansible command")
// TODO: implement
return nil
}

func New(log logger.Logger, conf conf.Config, session *session.Session) *ssm {
return &ssm{
log: log.Named("ssm"),
conf: conf,
cl: assm.New(session),
}
}

func (s ssm) provideInstanceIDs() []*string {
var instIDs []*string

ids := strings.Split(strings.TrimSpace(s.conf.AWSInstanceIDs), ",")
for _, i := range ids {
trimed := strings.TrimSpace(i)
instIDs = append(instIDs, &trimed)
}

s.log.Debug("Instance ids", "ids", spew.Sdump(instIDs))

return instIDs
}

func (s ssm) provideBashCommands() map[string][]*string {
var (
resp = map[string][]*string{}
shebang = "#!/bin/bash"
)

if s.conf.BashOneLiner != "" {
resp["commands"] = append(resp["commands"], &shebang)
resp["commands"] = append(resp["commands"], &s.conf.BashOneLiner)
} else if s.conf.BashFile != "" {
cmds, err := s.readBashFileAndProvideCommands()
if err != nil {
s.log.Fatalln("Could not provide bash commands", "err", err.Error())
}

for _, c := range cmds {
resp["commands"] = append(resp["commands"], c)
}
} else {
s.log.Fatalln("Bash command or bash script not specified")
}

s.log.Debug("Parsed commands from bash script", "cmds", spew.Sdump(resp))
return resp
}

func (s ssm) readBashFileAndProvideCommands() ([]*string, error) {
var cmds []*string

fileBytes, err := os.ReadFile(s.conf.BashFile)
if err != nil {
return nil, err
}

for _, cmdLine := range strings.Split(string(fileBytes), "\n") {
cmds = append(cmds, &cmdLine)
}

return cmds, nil
}

func (s ssm) waitForCmdExecutionComplete(cmdId *string, instId *string) error {
return s.cl.WaitUntilCommandExecutedWithContext(aws.BackgroundContext(), &assm.GetCommandInvocationInput{
CommandId: cmdId,
InstanceId: instId,
}, func(waiter *request.Waiter) {
waiter.Delay = request.ConstantWaiterDelay(time.Second * time.Duration(10))
})
}

func displayResults(instanceId *string, data *assm.GetCommandInvocationOutput) {
buff := bytes.Buffer{}

buff.WriteString(fmt.Sprintf("==== INSTANCE ID - %s =====\n", *instanceId))

if *data.StandardOutputContent != "" {
buff.WriteString("[COMMAND OUTPUT]\n")
buff.WriteString(*data.StandardOutputContent)
buff.WriteString("\n")
}

if *data.StandardErrorContent != "" {
buff.WriteString("[COMMAND ERROR]\n")
buff.WriteString(*data.StandardErrorContent)
}

buff.WriteString("====================\n\n")

fmt.Print(buff.String())
}
92 changes: 92 additions & 0 deletions conf/conf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package conf

import (
"errors"
"flag"
"log"
)

var (
ErrNoBashCMDOrScriptProvided = errors.New("bash cmd or script not provided")
ErrAnsiblePlaybookNotProvided = errors.New("ansible playbook not provided")
ErrEC2TagsOrIDsNotSpecified = errors.New("ec2 instance ids or tags not specified")
ErrEC2TagsAndIDsAreMutuallyExclusive = errors.New("ec2 instance ids and tags are mutually exclusive")
)

type Config struct {
LogLevel string
Mode string

BashOneLiner string
BashFile string

AnsiblePlaybook string

AWSProfile string
AWSRegion string

AWSInstanceIDs string
AWSInstanceTags string

CommandMaxWait int
}

func New() Config {
conf := DefaultConfig()

conf.processFlags()
if err := conf.validateFlags(); err != nil {
log.Fatalln(err)
}

return conf
}

func DefaultConfig() Config {
return Config{
LogLevel: "info",
Mode: "bash",
BashOneLiner: "",
BashFile: "",
AnsiblePlaybook: "",
AWSProfile: "",
AWSRegion: "",
AWSInstanceIDs: "",
AWSInstanceTags: "",
CommandMaxWait: 30,
}
}

func (c *Config) processFlags() {
flag.StringVar(&c.LogLevel, "log-level", c.LogLevel, "log output level")
flag.StringVar(&c.Mode, "mode", c.Mode, "running mode")
flag.StringVar(&c.BashOneLiner, "cmd", c.BashOneLiner, "bash command to run")
flag.StringVar(&c.BashFile, "script", c.BashFile, "bash script to run")
flag.StringVar(&c.AnsiblePlaybook, "playbook", c.AnsiblePlaybook, "ansible playbook to run")
flag.StringVar(&c.AWSProfile, "profile", c.AWSProfile, "aws profile")
flag.StringVar(&c.AWSRegion, "region", c.AWSRegion, "aws region")
flag.StringVar(&c.AWSInstanceIDs, "ids", c.AWSInstanceIDs, "comma delimited list of aws ec2 ids")
flag.StringVar(&c.AWSInstanceTags, "tags", c.AWSInstanceTags, "comma delimited list of ec2 tags")
flag.IntVar(&c.CommandMaxWait, "max-wait", c.CommandMaxWait, "maximum wait time in seconds for command execution")
flag.Parse()
}

func (c *Config) validateFlags() error {
if c.Mode == "bash" && c.BashFile == "" && c.BashOneLiner == "" {
return ErrNoBashCMDOrScriptProvided
}

if c.Mode == "ansible" && c.AnsiblePlaybook == "" {
return ErrAnsiblePlaybookNotProvided
}

if c.AWSInstanceIDs == "" && c.AWSInstanceTags == "" {
return ErrEC2TagsOrIDsNotSpecified
}

if c.AWSInstanceTags != "" && c.AWSInstanceIDs != "" {
return ErrEC2TagsAndIDsAreMutuallyExclusive
}

return nil
}
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ go 1.17

require (
github.com/aws/aws-sdk-go v1.44.79
github.com/davecgh/go-spew v1.1.1
github.com/hashicorp/go-hclog v1.2.2
github.com/pkg/errors v0.9.1
go.uber.org/fx v1.20.1
)

require (
github.com/fatih/color v1.13.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/dig v1.17.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect
)
Loading

0 comments on commit 391baae

Please sign in to comment.