-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f21da61
commit 391baae
Showing
9 changed files
with
513 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.