diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1756d90..5f1c786 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -24,7 +24,7 @@ jobs: - uses: actions/setup-go@v3 with: - go-version: 1.17.x + go-version: 1.21.x cache: true - uses: goreleaser/goreleaser-action@v2 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 5641820..228f884 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,8 +1,5 @@ -# This is an example .goreleaser.yml file with some sensible defaults. -# Make sure to check the documentation at https://goreleaser.com before: hooks: - # You may remove this if you don't use go modules. - go mod tidy builds: - env: diff --git a/Makefile b/Makefile index 5ce0baf..86a2367 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ .PHONY: lint lint: - golangci-lint run -E whitespace -E wsl -E wastedassign -E unconvert -E tparallel -E thelper -E stylecheck -E prealloc \ + golangci-lint run --fix -E whitespace -E wsl -E wastedassign -E unconvert -E tparallel -E thelper -E stylecheck -E prealloc \ -E predeclared -E nlreturn -E misspell -E makezero -E lll -E importas -E gosec -E gofmt -E goconst \ -E forcetypeassert -E dogsled -E dupl -E errname -E errorlint -E nolintlint --timeout 2m \ No newline at end of file diff --git a/README.md b/README.md index c25d8da..f5ec040 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,72 @@ # AWS Commander -A tool used for running bash scripts on AWS EC2 instances, leveraging AWS Systems Manager > Run Command feature. -User can load a bash script or define a single command, that will execute on all instances with defined instance ID. +A tool used for easier automation of the AWS EC2 instances, leveraging AWS Systems Manager - Run Command feature. +Supported scripts: +* One-liner bash command +* Bash script loaded from a local filesystem +* Ansible playbook loaded from a local filesystem + +The command/script/playbook will run across all EC2 instances simultaneously. +EC2 instances, for now, can be selected only by their IDs. Support for selection by tags will be +added in future versions. + ## Prerequisites -* The **AmazonSSMManagedInstanceCore** must be placed on all instances that need to be managed via this tool. -* AWS API credentials defined in *aws credentials* file or as environment variables +* The **AmazonSSMManagedInstanceCore** IAM role, attached on all EC2 instances. +* Authenticated AWS CLI session ## Usage ### AWS credentials -AWS credentials can be pulled from environment variables or from aws credentials file. -To define a which profile from credentials file should be used, set `aws-profile` flag. By default, it is set to `default`. -Environment variables with credentials that can be set: -* `AWS_ACCESS_KEY_ID` - the aws access key id -* `AWS_SECRET_ACCESS_KEY` - the access key secret -* `AWS_SESSION_TOKEN` - the session token (optional) - +AWS access must be authenticated via `aws cli`. ### General Parameters -* `aws-profile` - AWS profile as defined in *aws credentials* file. Default: `default` -* `aws-zone` - AWS zone in which EC2 instances reside. Default: `eu-central-1` -* `instances` - instance IDs, separated by comma (,). This is a mandatory flag. -* `log-level` - the level of logging output (info, debug, error). Default: `info` -* `output` - a file name to write the output result of a command/script. Default: `console output` -* `mode` - switch between modes - Bash script or Ansible playbook. Default: `bash` +* `log-level` - the level of logging output (`info`, `debug`, `error`). Default: `info` +* `mode` - commands running mode (`bash`, `ansible`) Default: `bash` +* `profile` - AWS profile as defined in *aws credentials* file. +* `region` - AWS region in which EC2 instances reside. +* `ids` - instance IDs, separated by comma (`,`). This is a mandatory flag. +* `max-wait` - maximum wait time in seconds to run the command Default: `30` +* `max-exec` - maximum wait time in seconds to get command result Default: `300` ### Running Bash scripts * `cmd` - one-liner bash command that will be executed on EC2 instances. * `script` - the location of bash script file that will run on EC2 instances. -* `mode` - for running bash scripts `mode` can be omitted as the default value is `bash` - -If both `cmd` and `script` flags are defined, `script` will take precedence, and `cmd` will be disregarded. +* `mode` - for running Bash script or oneliner `mode` can be omitted or set to `bash` #### Example ```bash +# AWS authentication +aws sso login --profile test-account + +# oneliner aws-commander -instances i-0bf9c273c67f684a0,i-011c9b3e3607a63b5,i-0e53e37f7b34517f5,i-0f02ca10faf8f349e -cmd "cd /tmp && ls -lah" -aws-profile test-account + +# or bash script +aws-commander -instances i-0bf9c273c67f684a0,i-011c9b3e3607a63b5,i-0e53e37f7b34517f5,i-0f02ca10faf8f349e -script ./script.sh -aws-profile test-account ``` ### Running Ansible Playbook * `playbook` - the location of Ansible playbook that will be executed on EC2 instances. +* `ansible-url` - the URL locaction of the Ansible playbook +* `extra-vars` - comma delimited, key value pairs of Ansible variables * `dryrun` - when set to true, Ansible playbook will run and the output will be shown, but - no data will be changed. + no data will be changed. Default: `false` * `mode` - for running Ansible playbook `mode` must be set to `ansible` -#### Ansible prerequisites -Every EC2 instance, that should run Ansible playbook, must have Ansible already installed. -If Ansible is not installed, the deployment will fail. -You can use `bash` mode to simply install Ansible from your OS package manager before running the playbook. - #### Example ```bash -## if Ansible is not installed on host - install Ansible -aws-commander -instances i-0bf9c273c67f684a0,i-011c9b3e3607a63b5,i-0e53e37f7b34517f5,i-0f02ca10faf8f349e -cmd "sudo apt install -y ansible" -aws-profile test-account -aws-zone us-west-2 -## run playbook -aws-commander -instances i-0bf9c273c67f684a0,i-011c9b3e3607a63b5,i-0e53e37f7b34517f5,i-0f02ca10faf8f349e -mode ansible -playbook scripts/nodes-restart.yaml -aws-profile test-account -aws-zone us-west-2 +# AWS authentication +aws sso login + +# run local playbook +aws-commander -instances i-0bf9c273c67f684a0,i-011c9b3e3607a63b5,i-0e53e37f7b34517f5,i-0f02ca10faf8f349e -mode ansible -playbook scripts/init.yaml -extra-vars foo=bar,faz=baz + +# or from url +aws-commander -instances i-0bf9c273c67f684a0,i-011c9b3e3607a63b5,i-0e53e37f7b34517f5,i-0f02ca10faf8f349e -mode ansible -ansible-url https://example.com/init.yaml -extra-vars foo=bar,faz=baz ``` #### Missing features -Currently, running the Ansible playbook from a remote location via URL / S3 is not supported. -It will be supported in the future release. +* Select EC2 instances using instance tags diff --git a/app/app.go b/app/app.go new file mode 100644 index 0000000..270e6ce --- /dev/null +++ b/app/app.go @@ -0,0 +1,31 @@ +package app + +import ( + "os" + + "github.com/Trapesys/aws-commander/aws" + "github.com/Trapesys/aws-commander/conf" + "github.com/Trapesys/aws-commander/logger" + "go.uber.org/fx" +) + +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) +} diff --git a/aws/aws.go b/aws/aws.go new file mode 100644 index 0000000..f0c2dc5 --- /dev/null +++ b/aws/aws.go @@ -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) +} diff --git a/aws/ssm/ansible.go b/aws/ssm/ansible.go new file mode 100644 index 0000000..b721a14 --- /dev/null +++ b/aws/ssm/ansible.go @@ -0,0 +1,91 @@ +package ssm + +import ( + "os" + "strings" + + "github.com/aws/aws-sdk-go/aws" + assm "github.com/aws/aws-sdk-go/service/ssm" + "github.com/davecgh/go-spew/spew" +) + +func (s ssm) RunAnsible() error { + s.log.Info("Running ssm ansible command") + + command, err := s.cl.SendCommand(&assm.SendCommandInput{ + DocumentName: aws.String("AWS-RunAnsiblePlaybook"), + DocumentVersion: aws.String("$LATEST"), + InstanceIds: s.provideInstanceIDs(), + Parameters: s.provideAnsibleCommands(), + TimeoutSeconds: &s.conf.CommandExecMaxWait, + }) + if err != nil { + return err + } + + s.log.Info("Ansible playbook deployed successfully") + s.log.Info("Waiting for results...") + + s.waitForCmdExecAndDisplayCmdOutput(command) + + return nil +} + +func (s ssm) provideAnsibleCommands() map[string][]*string { + var ( + trueStr = "True" + falseStr = "False" + resp = map[string][]*string{} + check = map[bool]*string{ + true: &trueStr, + false: &falseStr, + } + ) + + resp["check"] = []*string{check[s.conf.AnsibleDryRun]} + + if s.conf.AnsiblePlaybook != "" { + playbookStr, err := os.ReadFile(s.conf.AnsiblePlaybook) + if err != nil { + s.log.Fatalln("Could not read ansible playbook", "err", err.Error()) + } + + playbook := string(playbookStr) + + resp["playbook"] = []*string{&playbook} + } + + if s.conf.AnsibleURL != "" { + resp["playbookurl"] = []*string{&s.conf.AnsibleURL} + } + + if s.conf.AnsibleExtraVars != "" { + resp["extravars"] = []*string{s.processExtraVars()} + } + + s.log.Debug("Ansible params", "prams", spew.Sdump(resp)) + + return resp +} + +func (s ssm) processExtraVars() *string { + var ( + trimmedVars = make([]string, 0) + processedVars string + ) + + vars := strings.Split(strings.TrimSpace(s.conf.AnsibleExtraVars), ",") + for _, v := range vars { + trimmedVars = append(trimmedVars, strings.TrimSpace(v)) + } + + for _, tv := range trimmedVars { + processedVars += tv + " " + } + + processedVars = processedVars[:len(processedVars)-1] // trim last space char + + s.log.Debug("Processed extra vars", "vars", processedVars) + + return &processedVars +} diff --git a/aws/ssm/bash.go b/aws/ssm/bash.go new file mode 100644 index 0000000..0cece03 --- /dev/null +++ b/aws/ssm/bash.go @@ -0,0 +1,79 @@ +package ssm + +import ( + "os" + "strings" + + "github.com/aws/aws-sdk-go/aws" + assm "github.com/aws/aws-sdk-go/service/ssm" + "github.com/davecgh/go-spew/spew" +) + +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: &s.conf.CommandExecMaxWait, + }) + if err != nil { + return err + } + + s.log.Info("Bash command deployed successfully") + s.log.Info("Waiting for results...") + + s.waitForCmdExecAndDisplayCmdOutput(command) + + return nil +} + +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 = make([]*string, 0) + + fileBytes, err := os.ReadFile(s.conf.BashFile) + if err != nil { + return nil, err + } + + s.log.Debug("Script content read", "content", string(fileBytes)) + + for _, cmdLine := range strings.Split(string(fileBytes), "\n") { + cmdLine := cmdLine // closure capture + s.log.Debug("Script line read", "line", cmdLine) + + cmds = append(cmds, &cmdLine) + } + + return cmds, nil +} diff --git a/aws/ssm/ssm.go b/aws/ssm/ssm.go new file mode 100644 index 0000000..a15eddd --- /dev/null +++ b/aws/ssm/ssm.go @@ -0,0 +1,103 @@ +package ssm + +import ( + "bytes" + "fmt" + "strings" + "time" + + "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/aws/aws-sdk-go/aws/session" + assm "github.com/aws/aws-sdk-go/service/ssm" + "github.com/davecgh/go-spew/spew" +) + +type ssm struct { + log logger.Logger + conf conf.Config + + cl *assm.SSM +} + +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 = make([]*string, 0) + + 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) 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(s.conf.CommandResultMaxWait)) + }) +} + +func (s ssm) waitForCmdExecAndDisplayCmdOutput(command *assm.SendCommandOutput) { + var instIdsSuccess = make([]*string, 0) + + for _, instID := range command.Command.InstanceIds { + if err := s.waitForCmdExecutionComplete(command.Command.CommandId, instID); err != 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) + } + } +} + +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) + } + + if *data.StandardErrorContent != "" { + buff.WriteString("[COMMAND ERROR]\n") + buff.WriteString(*data.StandardErrorContent) + } + + if *data.StandardOutputContent == "" && *data.StandardErrorContent == "" { + buff.WriteString("NO CONTENT TO SHOW\n") + } + + buff.WriteString("====================\n\n") + + fmt.Print(buff.String()) +} diff --git a/conf/conf.go b/conf/conf.go new file mode 100644 index 0000000..84d4c04 --- /dev/null +++ b/conf/conf.go @@ -0,0 +1,133 @@ +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 + AnsibleURL string + AnsibleExtraVars string + AnsibleDryRun bool + + AWSProfile string + AWSRegion string + + AWSInstanceIDs string + AWSInstanceTags string + + CommandResultMaxWait int + CommandExecMaxWait int64 +} + +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: "", + AnsibleURL: "", + AWSProfile: "", + AWSRegion: "", + AWSInstanceIDs: "", + AWSInstanceTags: "", + AnsibleExtraVars: "", + CommandResultMaxWait: 30, + CommandExecMaxWait: 300, + AnsibleDryRun: false, + } +} + +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.AnsibleURL, "ansible-url", c.AnsibleURL, + "ansible url where the playbook can be read from", + ) + 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.CommandResultMaxWait, "max-wait", c.CommandResultMaxWait, + "maximum wait time in seconds for command execution", + ) + flag.Int64Var(&c.CommandExecMaxWait, "max-exec", c.CommandExecMaxWait, + "maximum command execution time in seconds", + ) + flag.BoolVar(&c.AnsibleDryRun, "dryrun", c.AnsibleDryRun, + "run ansible in dry-run mode", + ) + flag.StringVar(&c.AnsibleExtraVars, "extra-vars", c.AnsibleExtraVars, + "comma separated key value pairs for extra vars (foo=bar,fus=baz)", + ) + 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 +} diff --git a/framework/adapters/left/cmd/cmd.go b/framework/adapters/left/cmd/cmd.go deleted file mode 100644 index 4d8dfaf..0000000 --- a/framework/adapters/left/cmd/cmd.go +++ /dev/null @@ -1,125 +0,0 @@ -package cmd - -import ( - "flag" - "os" - "strings" - - "github.com/Trapesys/aws-commander/framework/adapters/types/cmd" - "github.com/Trapesys/aws-commander/framework/ports" - "github.com/hashicorp/go-hclog" -) - -type Adapter struct { - logger hclog.Logger - - buffInstanceFlag string -} - -func NewAdapter() ports.ICmd { - return &Adapter{} -} - -func (a *Adapter) GetFlags() cmd.Flags { - flag.StringVar(cmd.UserFlags.AwsZone.ValueString, - cmd.UserFlags.AwsZone.Name, - cmd.UserFlags.AwsZone.DefaultString, - cmd.UserFlags.AwsZone.Usage, - ) - flag.StringVar(cmd.UserFlags.BashScriptLocation.ValueString, - cmd.UserFlags.BashScriptLocation.Name, - cmd.UserFlags.BashScriptLocation.DefaultString, - cmd.UserFlags.BashScriptLocation.Usage, - ) - flag.StringVar(&a.buffInstanceFlag, - cmd.UserFlags.InstanceIDs.Name, - cmd.UserFlags.InstanceIDs.DefaultString, - cmd.UserFlags.InstanceIDs.Usage, - ) - flag.StringVar(cmd.UserFlags.LogLevel.ValueString, - cmd.UserFlags.LogLevel.Name, - cmd.UserFlags.LogLevel.DefaultString, - cmd.UserFlags.LogLevel.Usage, - ) - flag.StringVar(cmd.UserFlags.OutputLocation.ValueString, - cmd.UserFlags.OutputLocation.Name, - cmd.UserFlags.OutputLocation.DefaultString, - cmd.UserFlags.OutputLocation.Usage, - ) - flag.StringVar(cmd.UserFlags.FreeFormCmd.ValueString, - cmd.UserFlags.FreeFormCmd.Name, - cmd.UserFlags.FreeFormCmd.DefaultString, - cmd.UserFlags.FreeFormCmd.Usage, - ) - flag.StringVar(cmd.UserFlags.AwsProfile.ValueString, - cmd.UserFlags.AwsProfile.Name, - cmd.UserFlags.AwsProfile.DefaultString, - cmd.UserFlags.AwsProfile.Usage, - ) - flag.StringVar(cmd.UserFlags.Mode.ValueString, - cmd.UserFlags.Mode.Name, - cmd.UserFlags.Mode.DefaultString, - cmd.UserFlags.Mode.Usage, - ) - flag.StringVar(cmd.UserFlags.AnsiblePlaybook.ValueString, - cmd.UserFlags.AnsiblePlaybook.Name, - cmd.UserFlags.AnsiblePlaybook.DefaultString, - cmd.UserFlags.AnsiblePlaybook.Usage, - ) - flag.BoolVar(cmd.UserFlags.AnsibleDryRun.ValueBool, - cmd.UserFlags.AnsibleDryRun.Name, - cmd.UserFlags.AnsibleDryRun.DefaultBool, - cmd.UserFlags.AnsibleDryRun.Usage, - ) - flag.Parse() - - a.checkFlags() - - cmd.UserFlags.InstanceIDs.ValueStringArr = append(cmd.UserFlags.InstanceIDs.ValueStringArr, - strings.Split(a.buffInstanceFlag, ",")...) - - return cmd.UserFlags -} - -func (a *Adapter) WithLogger(logger hclog.Logger) ports.ICmd { - a.logger = logger.Named("cmd") - - return a -} - -func (a Adapter) isAllowedMode() bool { - for _, mode := range cmd.UserFlags.Mode.AllowedValuesStr { - if *cmd.UserFlags.Mode.ValueString == mode { - return true - } - } - - return false -} - -func (a *Adapter) checkFlags() { - // check if Instance ID is defined - if a.buffInstanceFlag == "" { - a.logger.Error("instance IDs not defined") - flag.PrintDefaults() - - os.Exit(1) - } - - // check if modes are allowed - if !a.isAllowedMode() { - a.logger.Error("only bash script and ansible playbook modes types are supported") - flag.PrintDefaults() - - os.Exit(1) - } - - // check if ansible playbook is defined - if *cmd.UserFlags.Mode.ValueString == "ansible" && - *cmd.UserFlags.AnsiblePlaybook.ValueString == "" { - a.logger.Error("running in Ansible mode but no Ansible Playbook file defined!") - flag.PrintDefaults() - - os.Exit(1) - } -} diff --git a/framework/adapters/left/localfs/localfs.go b/framework/adapters/left/localfs/localfs.go deleted file mode 100644 index 189e7f9..0000000 --- a/framework/adapters/left/localfs/localfs.go +++ /dev/null @@ -1,59 +0,0 @@ -package localfs - -import ( - "encoding/json" - "fmt" - "os" - - "github.com/Trapesys/aws-commander/framework/adapters/types/ssm" - "github.com/Trapesys/aws-commander/framework/ports" - "github.com/hashicorp/go-hclog" -) - -type Adapter struct { - logger hclog.Logger -} - -func NewAdapter() ports.ILocalFS { - return &Adapter{} -} - -func (a *Adapter) WithLogger(logger hclog.Logger) ports.ILocalFS { - a.logger = logger.Named("localfs") - - return a -} - -func (a Adapter) ReadBashScript(bashScriptLocation string) string { - fileBytes, err := os.ReadFile(bashScriptLocation) - if err != nil { - a.logger.Error("could not read file", "file", bashScriptLocation, "err", err.Error()) - os.Exit(1) - } - - return string(fileBytes) -} - -func (a Adapter) ReadAnsiblePlaybook(playbookLocation string) string { - // TODO: check if is yaml or yml file - fileBytes, err := os.ReadFile(playbookLocation) - if err != nil { - a.logger.Error("could not read file", "file", playbookLocation, "err", err.Error()) - os.Exit(1) - } - - return string(fileBytes) -} - -func (a Adapter) WriteRunCommandOutput(cmdOutput ssm.Instances, outputLocation string) error { - jsonBuff, err := json.MarshalIndent(cmdOutput, "", " ") - if err != nil { - return fmt.Errorf("could not marshal command output to json: %w", err) - } - - if wrErr := os.WriteFile(outputLocation, jsonBuff, 0600); wrErr != nil { - return fmt.Errorf("could not write file to disk: %w", wrErr) - } - - return nil -} diff --git a/framework/adapters/right/ssm/helpers.go b/framework/adapters/right/ssm/helpers.go deleted file mode 100644 index 875b1c1..0000000 --- a/framework/adapters/right/ssm/helpers.go +++ /dev/null @@ -1,73 +0,0 @@ -package ssm - -import ( - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ssm" -) - -type commandType string - -var generateCommand map[commandType]func() *ssm.SendCommandInput - -// these should reflect flag options for -mode flag - bash by default -const ( - bashScript commandType = "bash" - ansiblePlaybook commandType = "ansible" -) - -// prepareCommand sets the function which initializes command based on the flag input -func (a *Adapter) prepareCommand() map[commandType]func() *ssm.SendCommandInput { - generateCommand = map[commandType]func() *ssm.SendCommandInput{ - bashScript: a.runBashScript, - ansiblePlaybook: a.runAnsiblePlaybook, - } - - return generateCommand -} - -// runBashScript initializes AWS-RunShellScript document which will run a bash script on nodes -func (a *Adapter) runBashScript() *ssm.SendCommandInput { - // if we have instance IDs, else if we have tags - if a.instanceIDs != nil { - return &ssm.SendCommandInput{ - DocumentName: aws.String("AWS-RunShellScript"), - DocumentVersion: aws.String("$LATEST"), - InstanceIds: a.instanceIDs, - Parameters: a.commands, - TimeoutSeconds: aws.Int64(300), - } - } else if a.targets != nil { - return &ssm.SendCommandInput{ - DocumentName: aws.String("AWS-RunShellScript"), - DocumentVersion: aws.String("$LATEST"), - Targets: a.targets, - Parameters: a.commands, - TimeoutSeconds: aws.Int64(300), - } - } - - return nil -} - -func (a *Adapter) runAnsiblePlaybook() *ssm.SendCommandInput { - // if we have instance IDs, else if we have tags - if a.instanceIDs != nil { - return &ssm.SendCommandInput{ - DocumentName: aws.String("AWS-RunAnsiblePlaybook"), - DocumentVersion: aws.String("$LATEST"), - InstanceIds: a.instanceIDs, - Parameters: a.commands, - TimeoutSeconds: aws.Int64(300), - } - } else if a.targets != nil { - return &ssm.SendCommandInput{ - DocumentName: aws.String("AWS-RunAnsiblePlaybook"), - DocumentVersion: aws.String("$LATEST"), - Targets: a.targets, - Parameters: a.commands, - TimeoutSeconds: aws.Int64(300), - } - } - - return nil -} diff --git a/framework/adapters/right/ssm/ssm.go b/framework/adapters/right/ssm/ssm.go deleted file mode 100644 index 5c03cca..0000000 --- a/framework/adapters/right/ssm/ssm.go +++ /dev/null @@ -1,207 +0,0 @@ -package ssm - -import ( - "fmt" - "strings" - "time" - - ssm2 "github.com/Trapesys/aws-commander/framework/adapters/types/ssm" - "github.com/Trapesys/aws-commander/framework/ports" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/ssm" - "github.com/hashicorp/go-hclog" -) - -// Adapter is the adapter for SSM port -type Adapter struct { - ssmSession *ssm.SSM - logger hclog.Logger - instanceIDs []*string - targets []*ssm.Target - mode commandType - ansiblePlaybookOpts *ports.AnsiblePlaybookOpts - - commands -} - -type commands map[string][]*string - -// NewAdapter returns new ssm instance -func NewAdapter() ports.ISSMPort { - return &Adapter{ - commands: make(map[string][]*string), - instanceIDs: nil, - targets: nil, - } -} - -func newInstances(instances []string) []*string { - inst := []*string{} - - for _, ins := range instances { - inst = append(inst, aws.String(ins)) - } - - return inst -} - -func (a *Adapter) WithAWSSession(awsSession *session.Session) ports.ISSMPort { - a.ssmSession = ssm.New(awsSession) - - return a -} - -func (a *Adapter) WithLogger(logger hclog.Logger) ports.ISSMPort { - a.logger = logger.Named("ssm") - - return a -} - -func (a *Adapter) WithInstances(instanceIDs []string) ports.ISSMPort { - a.instanceIDs = newInstances(instanceIDs) - - return a -} - -func (a *Adapter) WithInstanceTags(tagName string, tagValues []string) ports.ISSMPort { - val := []*string{} - for _, tag := range tagValues { - val = append(val, aws.String(tag)) - } - - tgName := "tag:" + tagName - - a.targets = append(a.targets, &ssm.Target{ - Key: &tgName, - Values: val, - }) - - a.logger.Debug("target tags set", "target", fmt.Sprintf("%+v\n", a.targets)) - - // TODO: this feature does not work properly at this moment - return a -} - -func (a *Adapter) RunCommand() ssm2.Instances { - var command *ssm.SendCommandInput - - responseData := ssm2.Instances{} - prepareCommandFunc := a.prepareCommand() - - if command = prepareCommandFunc[a.mode](); command == nil { - a.logger.Error("could not find instance or tag to run the commands on") - - return responseData - } - // Send Command - out, err := a.ssmSession.SendCommand(command) - if err != nil { - a.logger.Error("could not run SSM command", "err", err.Error()) - - return responseData - } - - a.logger.Debug("send command call response", "response", out.String()) - - // TODO: when using with tags, we do not have any instance ID - // TODO: we need some kind of mechanism to get instance ids from tags or not use Tag at all - - // wait for it to complete before returning - for _, instance := range out.Command.InstanceIds { - if cmdTimeoutErr := a.ssmSession.WaitUntilCommandExecutedWithContext( - aws.BackgroundContext(), - &ssm.GetCommandInvocationInput{ - CommandId: out.Command.CommandId, - InstanceId: instance, - PluginName: nil, - }, func(waiter *request.Waiter) { - waiter.Delay = request.ConstantWaiterDelay(10 * time.Second) - waiter.MaxAttempts = 60 - }, - ); cmdTimeoutErr != nil { - //nolint - if awsErr, ok := cmdTimeoutErr.(awserr.Error); ok { - if awsErr.Code() == request.WaiterResourceNotReadyErrorCode { - a.logger.Error( - "error running ansible playbook", - "instanceID", *instance, - "commandID", *out.Command.CommandId, - ) - } else if awsErr.Code() == request.ErrCodeResponseTimeout { - a.logger.Error( - "timeout reached while waiting for command to finish", - "instanceID", *instance, - "commandID", *out.Command.CommandId, - ) - } - } - } - } - // and parse and return output data - for _, instanceID := range out.Command.InstanceIds { - var output, errorOutput string - output, errorOutput = a.GetCommandOutput(out.Command.CommandId, instanceID) - inst := ssm2.Instance{ - ID: *instanceID, - CommandOutput: output, - ErrorOutput: errorOutput, - } - - responseData.Instance = append(responseData.Instance, inst) - } - - return responseData -} - -func (a *Adapter) GetCommandOutput(cmdID, instanceID *string) (string, string) { - out, err := a.ssmSession.GetCommandInvocation(&ssm.GetCommandInvocationInput{ - CommandId: cmdID, - InstanceId: instanceID, - PluginName: nil, - }) - if err != nil { - a.logger.Error("could not get command output", "cmd_id", cmdID, "err", err.Error()) - - return "", "" - } - - return *out.StandardOutputContent, *out.StandardErrorContent -} - -func (a *Adapter) WithCommands(commandsString string) ports.ISSMPort { - for _, cmd := range strings.Split(commandsString, "\n") { - a.commands["commands"] = append(a.commands["commands"], aws.String(cmd)) - } - - for _, cmdPtr := range a.commands["commands"] { - a.logger.Debug("parsed commands to run", "commands", *cmdPtr) - } - - return a -} - -func (a *Adapter) WithFreeFormCommand(cmd string) ports.ISSMPort { - shell := "#!/bin/bash" - a.commands["commands"] = append(a.commands["commands"], &shell) - a.commands["commands"] = append(a.commands["commands"], &cmd) - - return a -} - -func (a *Adapter) WithMode(mode string) { - a.mode = commandType(mode) -} - -func (a *Adapter) WithAnsiblePlaybook(opts *ports.AnsiblePlaybookOpts) ports.ISSMPort { - a.ansiblePlaybookOpts = opts - - a.commands["playbook"] = []*string{&opts.Playbook} - a.commands["playbookurl"] = []*string{&opts.PlaybookURL} - a.commands["extravars"] = []*string{&opts.ExtraVars} - a.commands["check"] = []*string{&opts.Check} - - return a -} diff --git a/framework/adapters/types/cmd/types.go b/framework/adapters/types/cmd/types.go deleted file mode 100644 index 2966a8d..0000000 --- a/framework/adapters/types/cmd/types.go +++ /dev/null @@ -1,94 +0,0 @@ -package cmd - -type Flags struct { - AwsZone FlagDetails - InstanceIDs FlagDetails - BashScriptLocation FlagDetails - LogLevel FlagDetails - OutputLocation FlagDetails - FreeFormCmd FlagDetails - AwsProfile FlagDetails - Mode FlagDetails - AnsiblePlaybook FlagDetails - AnsibleDryRun FlagDetails -} - -type FlagDetails struct { - Name string - Usage string - - DefaultString string - DefaultInt int - DefaultBool bool - - ValueString *string - ValueStringArr []string - ValueInt *int - ValueBool *bool - - AllowedValuesStr []string -} - -var UserFlags = Flags{ - AwsZone: FlagDetails{ - Name: "aws-zone", - Usage: "aws zone where instances reside", - DefaultString: "eu-central-1", - ValueString: new(string), - }, - InstanceIDs: FlagDetails{ - Name: "instances", - Usage: "instance IDs, separated by comma (,)", - DefaultString: "", - ValueStringArr: make([]string, 0), - }, - BashScriptLocation: FlagDetails{ - Name: "script", - Usage: "the location of the script to run", - DefaultString: "", - ValueString: new(string), - }, - LogLevel: FlagDetails{ - Name: "log-level", - Usage: "log output level", - DefaultString: "info", - ValueString: new(string), - }, - OutputLocation: FlagDetails{ - Name: "output", - Usage: "the location of file to write json output (default: output to console)", - DefaultString: "", - ValueString: new(string), - }, - FreeFormCmd: FlagDetails{ - Name: "cmd", - Usage: "freeform command, a single line bash command to be executed", - DefaultString: "", - ValueString: new(string), - }, - AwsProfile: FlagDetails{ - Name: "aws-profile", - Usage: "aws credentials profile", - DefaultString: "default", - ValueString: new(string), - }, - Mode: FlagDetails{ - Name: "mode", - Usage: "set command mode - bash script or ansible playbook", - DefaultString: "bash", - ValueString: new(string), - AllowedValuesStr: []string{"bash", "ansible"}, - }, - AnsiblePlaybook: FlagDetails{ - Name: "playbook", - Usage: "the location of Ansible playbook file", - DefaultString: "", - ValueString: new(string), - }, - AnsibleDryRun: FlagDetails{ - Name: "dryrun", - Usage: "run Ansible script without changing any actual data", - DefaultBool: false, - ValueBool: new(bool), - }, -} diff --git a/framework/adapters/types/ssm/types.go b/framework/adapters/types/ssm/types.go deleted file mode 100644 index 36c0b15..0000000 --- a/framework/adapters/types/ssm/types.go +++ /dev/null @@ -1,11 +0,0 @@ -package ssm - -type Instances struct { - Instance []Instance `json:"instance"` -} - -type Instance struct { - ID string `json:"id"` - CommandOutput string `json:"commandOutput"` - ErrorOutput string `json:"errorOutput"` -} diff --git a/framework/ports/cmd_port.go b/framework/ports/cmd_port.go deleted file mode 100644 index 237baf7..0000000 --- a/framework/ports/cmd_port.go +++ /dev/null @@ -1,11 +0,0 @@ -package ports - -import ( - "github.com/Trapesys/aws-commander/framework/adapters/types/cmd" - "github.com/hashicorp/go-hclog" -) - -type ICmd interface { - WithLogger(logger hclog.Logger) ICmd - GetFlags() cmd.Flags -} diff --git a/framework/ports/localfs_port.go b/framework/ports/localfs_port.go deleted file mode 100644 index 1f8c689..0000000 --- a/framework/ports/localfs_port.go +++ /dev/null @@ -1,17 +0,0 @@ -package ports - -import ( - "github.com/Trapesys/aws-commander/framework/adapters/types/ssm" - "github.com/hashicorp/go-hclog" -) - -type ILocalFS interface { - // ReadBashScript is a file reader intended for reading bash scripts. - ReadBashScript(bashScriptLocation string) string - // ReadAnsiblePlaybook is a file reader intended for reading ansible playbook yaml/yml files - ReadAnsiblePlaybook(playbookLocation string) string - // WithLogger injects a logger instance - WithLogger(logger hclog.Logger) ILocalFS - // WriteRunCommandOutput writes output result from ssm.RunCommand - WriteRunCommandOutput(cmdOutput ssm.Instances, outputLocation string) error -} diff --git a/framework/ports/ssm_port.go b/framework/ports/ssm_port.go deleted file mode 100644 index 74f695b..0000000 --- a/framework/ports/ssm_port.go +++ /dev/null @@ -1,52 +0,0 @@ -package ports - -import ( - "github.com/Trapesys/aws-commander/framework/adapters/types/ssm" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/hashicorp/go-hclog" -) - -type ISSMPort interface { - // RunCommand runs the commands - RunCommand() ssm.Instances - - // WithMode sets the mode to run command. This defines if Ansible or Bash script will be run. - WithMode(mode string) - - // GetCommandOutput outputs the command result - GetCommandOutput(commandID, instanceID *string) (string, string) - - // WithLogger builder builds the logger instance - WithLogger(logger hclog.Logger) ISSMPort - - // WithCommands takes in the commands to execute on the instance. - // - // Commands are split by new line character - WithCommands(commandsString string) ISSMPort - - // WithFreeFormCommand takes in a single bash command to be executed - WithFreeFormCommand(cmd string) ISSMPort - - // WithAnsiblePlaybook takes in the location of Ansible playbook - WithAnsiblePlaybook(opts *AnsiblePlaybookOpts) ISSMPort - - // WithInstances builder sets the instance IDs to run the command on - // - // Can be omitted if WithInstanceTags is set - WithInstances(instanceIDs []string) ISSMPort - - // WithInstanceTags builder sets the instance tags to run the command on - // - // Can be omitted if WithInstances is set - WithInstanceTags(tagName string, tagValues []string) ISSMPort - - // WithAWSSession builder builds the SSM adapter with AWS session - WithAWSSession(awsSession *session.Session) ISSMPort -} - -type AnsiblePlaybookOpts struct { - Playbook string - PlaybookURL string - ExtraVars string - Check string -} diff --git a/go.mod b/go.mod index 724b698..291aa68 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,10 @@ 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 ( @@ -12,5 +15,9 @@ require ( 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 ) diff --git a/go.sum b/go.sum index 9ac632c..f95afd6 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,8 @@ github.com/aws/aws-sdk-go v1.44.79 h1:IZCtfBq9VlJ1Eu34I+2Y76q+XkvTtZYbEwaoVM1gzoA= github.com/aws/aws-sdk-go v1.44.79/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 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= @@ -11,32 +14,82 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y 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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI= +go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= +go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= +go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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/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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/internal/adapters/app/app.go b/internal/adapters/app/app.go deleted file mode 100644 index 840a397..0000000 --- a/internal/adapters/app/app.go +++ /dev/null @@ -1,173 +0,0 @@ -package app - -import ( - "fmt" - "os" - - "github.com/Trapesys/aws-commander/framework/adapters/types/cmd" - fports "github.com/Trapesys/aws-commander/framework/ports" - "github.com/Trapesys/aws-commander/internal/ports" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/hashicorp/go-hclog" -) - -type Adapter struct { - coreAdapter ports.CorePort - localFSAdapter fports.ILocalFS - ssmAdapter fports.ISSMPort - cmdAdapter fports.ICmd - baseLogger hclog.Logger - - flags cmd.Flags -} - -func (a *Adapter) getAWSOptions() session.Options { - // use env vars if set - if os.Getenv("AWS_ACCESS_KEY_ID") != "" && - os.Getenv("AWS_SECRET_ACCESS_KEY") != "" { - a.baseLogger.Info("using aws secrets found in env vars") - - return session.Options{ - Config: aws.Config{ - Credentials: credentials.NewStaticCredentials( - os.Getenv("AWS_ACCESS_KEY_ID"), - os.Getenv("AWS_SECRET_ACCESS_KEY"), - os.Getenv("AWS_SESSION_TOKEN"), - ), - Region: a.flags.AwsZone.ValueString, - }, - } - } - - // otherwise, use profile from aws credential file - awsProfile := os.Getenv("AWS_PROFILE") - if awsProfile == "" { - awsProfile = *a.flags.AwsProfile.ValueString - } - - awsRegion := os.Getenv("AWS_REGION") - if awsRegion == "" { - awsRegion = *a.flags.AwsZone.ValueString - } - - a.baseLogger.Info("reading aws credentials from", "profile", awsProfile, "region", awsRegion) - - return session.Options{ - Config: aws.Config{ - Region: &awsRegion, - }, - Profile: awsProfile, - SharedConfigState: session.SharedConfigEnable, - } -} - -func (a *Adapter) Init() ports.IApp { - /// CMD /// - a.flags = a.cmdAdapter.WithLogger(a.baseLogger).GetFlags() - ////////// - // set log level - a.baseLogger.SetLevel(hclog.LevelFromString(*a.flags.LogLevel.ValueString)) - - // CORE // - // init core - a.coreAdapter.WithAWSSessionOptions(a.getAWSOptions()).WithLogger(a.baseLogger) - ///////// - - // SSM // - // init ssm session - a.ssmAdapter.WithAWSSession(a.coreAdapter.GetAWSSession()).WithLogger(a.baseLogger) - - // MODE // - // init command mode ( bash or ansible ) - a.ssmAdapter.WithMode(*a.flags.Mode.ValueString) - - // init ssm instances - a.ssmAdapter.WithInstances(a.flags.InstanceIDs.ValueStringArr) - - // init ssm commands - switch *a.flags.Mode.ValueString { - case "bash": - a.baseLogger.Debug("mode set to Bash") - - if *a.flags.BashScriptLocation.ValueString != "" { - a.baseLogger.Debug("running Bash script", "script_location", *a.flags.BashScriptLocation.ValueString) - a.ssmAdapter.WithCommands( - a.localFSAdapter. - WithLogger(a.baseLogger). - ReadBashScript(*a.flags.BashScriptLocation.ValueString), - ) - - break - } - - if *a.flags.FreeFormCmd.ValueString != "" { - a.baseLogger.Debug("freeform command found", "cmd", *a.flags.FreeFormCmd.ValueString) - a.ssmAdapter.WithFreeFormCommand(*a.flags.FreeFormCmd.ValueString) - - break - } - - a.baseLogger.Error("when Bash mode is selected, script location and/or freeform command can't be empty") - - case "ansible": - a.baseLogger.Debug("mode set to Ansible") - - // TODO: implement PlaybookURL and ExtraVars - isDryRun := "False" - if *a.flags.AnsibleDryRun.ValueBool { - isDryRun = "True" - } - - a.ssmAdapter.WithAnsiblePlaybook(&fports.AnsiblePlaybookOpts{ - Playbook: a.localFSAdapter.ReadAnsiblePlaybook(*a.flags.AnsiblePlaybook.ValueString), - PlaybookURL: "", - ExtraVars: "", - Check: isDryRun, - }) - default: - a.baseLogger.Error("could not find a command to execute, " + - "script location or cmd flag must be defined") - os.Exit(1) - } - ///////////// - - return a -} - -func NewAdapter( - core ports.CorePort, - local fports.ILocalFS, - ssm fports.ISSMPort, - cmd fports.ICmd) ports.IApp { - return &Adapter{ - coreAdapter: core, - localFSAdapter: local, - ssmAdapter: ssm, - cmdAdapter: cmd, - } -} - -func (a *Adapter) WithLogger(logger hclog.Logger) ports.IApp { - a.baseLogger = logger - - return a -} - -func (a *Adapter) RunCommand() error { - cmdResult := a.ssmAdapter.RunCommand() - if *a.flags.OutputLocation.ValueString != "" { - if err := a.localFSAdapter.WriteRunCommandOutput(cmdResult, *a.flags.OutputLocation.ValueString); err != nil { - return fmt.Errorf("could not write command result to file: %w", err) - } - - a.baseLogger.Info("command output written to file", "filename", *a.flags.OutputLocation.ValueString) - os.Exit(0) - } - - // output to console - fmt.Printf("%+v\n", cmdResult) - - return nil -} diff --git a/internal/adapters/core/core.go b/internal/adapters/core/core.go deleted file mode 100644 index a42fc1c..0000000 --- a/internal/adapters/core/core.go +++ /dev/null @@ -1,43 +0,0 @@ -package core - -import ( - "github.com/Trapesys/aws-commander/internal/ports" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/hashicorp/go-hclog" -) - -// Adapter plugs into Core port -type Adapter struct { - awsSession *session.Session - logger hclog.Logger -} - -// NewAdapter creates a new adapter -func NewAdapter() ports.CorePort { - return &Adapter{} -} - -// WithLogger builder builds a new logger instance -func (a *Adapter) WithLogger(logger hclog.Logger) ports.CorePort { - a.logger = logger.Named("core") - - return a -} - -// WithAWSSessionOptions builder builds a new AWS session instance -func (a *Adapter) WithAWSSessionOptions(awsOpt session.Options) ports.CorePort { - var err error - - a.awsSession, err = session.NewSessionWithOptions(awsOpt) - if err != nil { - a.logger.Error("could not create new AWS session", "err", err.Error()) - - return nil - } - - return a -} - -func (a *Adapter) GetAWSSession() *session.Session { - return a.awsSession -} diff --git a/internal/ports/app_port.go b/internal/ports/app_port.go deleted file mode 100644 index 55068e1..0000000 --- a/internal/ports/app_port.go +++ /dev/null @@ -1,11 +0,0 @@ -package ports - -import ( - "github.com/hashicorp/go-hclog" -) - -type IApp interface { - RunCommand() error - Init() IApp - WithLogger(logger hclog.Logger) IApp -} diff --git a/internal/ports/core_port.go b/internal/ports/core_port.go deleted file mode 100644 index 5f21acc..0000000 --- a/internal/ports/core_port.go +++ /dev/null @@ -1,12 +0,0 @@ -package ports - -import ( - "github.com/aws/aws-sdk-go/aws/session" - "github.com/hashicorp/go-hclog" -) - -type CorePort interface { - WithLogger(logger hclog.Logger) CorePort - WithAWSSessionOptions(awsOpt session.Options) CorePort - GetAWSSession() *session.Session -} diff --git a/logger/hclog.go b/logger/hclog.go new file mode 100644 index 0000000..dbcddb4 --- /dev/null +++ b/logger/hclog.go @@ -0,0 +1,78 @@ +package logger + +import ( + "os" + + "github.com/Trapesys/aws-commander/conf" + "github.com/hashicorp/go-hclog" +) + +type hlog struct { + log hclog.Logger +} + +func (h *hlog) Error(msg string, args ...interface{}) { + if len(args) == 0 { + h.log.Error(msg) + + return + } + + h.log.Error(msg, args...) +} + +func (h *hlog) Warn(msg string, args ...interface{}) { + if len(args) == 0 { + h.log.Warn(msg) + + return + } + + h.log.Warn(msg, args...) +} + +func (h *hlog) Info(msg string, args ...interface{}) { + if len(args) == 0 { + h.log.Info(msg) + + return + } + + h.log.Info(msg, args...) +} + +func (h *hlog) Debug(msg string, args ...interface{}) { + if len(args) == 0 { + h.log.Debug(msg) + + return + } + + h.log.Debug(msg, args...) +} + +func (h *hlog) Fatalln(msg string, args ...interface{}) { + if len(args) == 0 { + h.log.Error(msg) + } else { + h.log.Error(msg, args...) + } + + os.Exit(1) +} + +func (h *hlog) Named(name string) Logger { + h.log = h.log.Named(name) + + return h +} + +func New(conf conf.Config) Logger { + return &hlog{ + log: hclog.New(&hclog.LoggerOptions{ + Name: "aws-commander", + Level: hclog.LevelFromString(conf.LogLevel), + Color: hclog.AutoColor, + }), + } +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..16fcc56 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,10 @@ +package logger + +type Logger interface { + Error(msg string, args ...interface{}) + Warn(msg string, args ...interface{}) + Info(msg string, args ...interface{}) + Debug(msg string, args ...interface{}) + Fatalln(msg string, args ...interface{}) + Named(name string) Logger +} diff --git a/main.go b/main.go index e29c898..05c9d4b 100644 --- a/main.go +++ b/main.go @@ -1,34 +1,7 @@ package main -import ( - "os" - - "github.com/Trapesys/aws-commander/framework/adapters/left/cmd" - "github.com/Trapesys/aws-commander/framework/adapters/left/localfs" - "github.com/Trapesys/aws-commander/framework/adapters/right/ssm" - "github.com/Trapesys/aws-commander/internal/adapters/app" - "github.com/Trapesys/aws-commander/internal/adapters/core" - "github.com/hashicorp/go-hclog" -) +import "github.com/Trapesys/aws-commander/app" func main() { - // init logger instance - logger := hclog.New(&hclog.LoggerOptions{ - Name: "aws-commander", - Level: hclog.NoLevel, - }) - - // inject adapters into App - commander := app.NewAdapter( - core.NewAdapter(), - localfs.NewAdapter(), - ssm.NewAdapter(), - cmd.NewAdapter(), - ).WithLogger(logger).Init() - - // run command and check for error - if err := commander.RunCommand(); err != nil { - logger.Error("could not run command: ", "err", err.Error()) - os.Exit(1) - } + app.Run() }