diff --git a/commands/aws_lambda.go b/commands/aws_lambda.go index b2c6269..09503d2 100644 --- a/commands/aws_lambda.go +++ b/commands/aws_lambda.go @@ -70,7 +70,7 @@ var invokeCmd = &cobra.Command{ return } - sess, err := manager.GetSess(run.profile) + sess, err := GetSession() utils.HandleError(err) f := awsLambda{name: args[0]} diff --git a/commands/commands.go b/commands/commands.go index 647e3ea..5eee1c7 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -73,7 +73,7 @@ var ( // Get Project & AWS Region arrow := log.ColorString("->", "magenta") project = utils.GetInput(fmt.Sprintf("%s Enter your Project name", arrow), "qaz-project") - region = utils.GetInput(fmt.Sprintf("%s Enter AWS Region", arrow), "eu-west-1") + region := utils.GetInput(fmt.Sprintf("%s Enter AWS Region", arrow), "eu-west-1") // set target paths c := filepath.Join(target, "config.yml") diff --git a/commands/config.go b/commands/config.go index 999d719..bcb97c6 100644 --- a/commands/config.go +++ b/commands/config.go @@ -5,6 +5,8 @@ import ( yaml "gopkg.in/yaml.v2" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" stks "github.com/daidokoro/qaz/stacks" "github.com/daidokoro/hcl" @@ -14,7 +16,7 @@ import ( func Configure(confSource string, conf string) (err error) { // set config session - config.Session, err = manager.GetSess(run.profile) + config.Session, err = GetSession() if err != nil { return } @@ -51,6 +53,7 @@ func Configure(confSource string, conf string) (err error) { stacks.Add(s, &stks.Stack{ Name: s, Profile: v.Profile, + Region: v.Region, DependsOn: v.DependsOn, Policy: v.Policy, Source: v.Source, @@ -68,7 +71,24 @@ func Configure(confSource string, conf string) (err error) { stacks.MustGet(s).SetStackName() // set session - stacks.MustGet(s).Session, err = manager.GetSess(stacks.MustGet(s).Profile) + stacks.MustGet(s).Session, err = GetSession(func(opts *session.Options) { + if stacks.MustGet(s).Profile != "" { + opts.Profile = stacks.MustGet(s).Profile + } + + // use config region + if config.Region != "" { + opts.Config.Region = aws.String(config.Region) + } + + // stack region trumps all other regions if-set + if stacks.MustGet(s).Region != "" { + opts.Config.Region = aws.String(stacks.MustGet(s).Region) + } + + return + }) + if err != nil { return } diff --git a/commands/deploy.go b/commands/deploy.go index 312fca2..fa329d1 100644 --- a/commands/deploy.go +++ b/commands/deploy.go @@ -184,7 +184,7 @@ var ( PreRun: initialise, Run: func(cmd *cobra.Command, args []string) { - if len(args) < 1 { + if len(args) < 1 && !run.all { log.Warn("No stack specified for termination") return } @@ -192,13 +192,20 @@ var ( err := Configure(run.cfgSource, "") utils.HandleError(err) - if !run.all { - for _, s := range args { - if _, ok := stacks.Get(s); !ok { - utils.HandleError(fmt.Errorf("stacks [%s] not found in config", s)) - } - stacks.MustGet(s).Actioned = true + // select actioned stacks + for _, s := range args { + if _, ok := stacks.Get(s); !ok { + utils.HandleError(fmt.Errorf("stacks [%s] not found in config", s)) } + stacks.MustGet(s).Actioned = true + } + + // action stacks if all + if run.all { + stacks.Range(func(_ string, s *stks.Stack) bool { + s.Actioned = true + return true + }) } // Terminate Stacks diff --git a/commands/functions.go b/commands/functions.go index de68a7c..7ba956d 100644 --- a/commands/functions.go +++ b/commands/functions.go @@ -14,6 +14,7 @@ import ( "encoding/json" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/kms" ) @@ -21,7 +22,7 @@ import ( var ( kmsEncrypt = func(kid string, text string) string { log.Debug("running template function: [kms_encrypt]") - sess, err := manager.GetSess(run.profile) + sess, err := GetSession() utils.HandleError(err) svc := kms.New(sess) @@ -39,7 +40,7 @@ var ( kmsDecrypt = func(cipher string) string { log.Debug("running template function: [kms_decrypt]") - sess, err := manager.GetSess(run.profile) + sess, err := GetSession() utils.HandleError(err) svc := kms.New(sess) @@ -68,14 +69,15 @@ var ( s3Read = func(url string, profile ...string) string { log.Debug("Calling Template Function [S3Read] with arguments: %s", url) - var p = run.profile - if len(profile) < 1 { - log.Warn("No Profile specified for S3read, using: %s", p) - } else { - p = profile[0] - } + sess, err := GetSession(func(opts *session.Options) { + if len(profile) < 1 { + log.Warn("No Profile specified for S3read, using: %s", run.profile) + return + } + opts.Profile = profile[0] + return + }) - sess, err := manager.GetSess(p) utils.HandleError(err) resp, err := bucket.S3Read(url, sess) @@ -93,7 +95,7 @@ var ( f.payload = []byte(payload) } - sess, err := manager.GetSess(run.profile) + sess, err := GetSession() utils.HandleError(err) err = f.Invoke(sess) @@ -190,7 +192,11 @@ var ( log.Debug("Deploy-Time function resolving: %s", target) req := strings.Split(target, "::") - s := stacks.MustGet(req[0]) + s, ok := stacks.Get(req[0]) + if !ok { + utils.HandleError(fmt.Errorf("stack_output errror: stack [%s] not found", req[0])) + } + utils.HandleError(s.Outputs()) for _, i := range s.Output.Stacks { @@ -208,7 +214,7 @@ var ( log.Debug("Deploy-Time function resolving: %s", target) req := strings.Split(target, "::") - sess, err := manager.GetSess(run.profile) + sess, err := GetSession() utils.HandleError(err) s := stks.Stack{ diff --git a/commands/init.go b/commands/init.go index cf103b6..584b8f8 100644 --- a/commands/init.go +++ b/commands/init.go @@ -23,29 +23,35 @@ func init() { gitDeployCmd.Flags().StringVarP(&run.gitpass, "password", "", "", "git password") gitDeployCmd.Flags().StringVarP(&run.gitrsa, "ssh-rsa", "", filepath.Join(os.Getenv("HOME"), ".ssh/id_rsa"), "path to git SSH id_rsa") + // Define Git Status Command + gitStatusCmd.Flags().StringVarP(&run.gitrsa, "ssh-rsa", "", filepath.Join(os.Getenv("HOME"), ".ssh/id_rsa"), "path to git SSH id_rsa") + gitStatusCmd.Flags().StringVarP(&run.gitpass, "password", "", "", "git password") + gitStatusCmd.Flags().StringVarP(&run.gituser, "user", "u", "", "git username") + // Define Terminate Flags terminateCmd.Flags().BoolVarP(&run.all, "all", "A", false, "terminate all stacks") // Define Output Flags outputsCmd.Flags().StringVarP(&run.profile, "profile", "p", "default", "configured aws profile") - // Define Exports Flags - exportsCmd.Flags().StringVarP(®ion, "region", "r", "eu-west-1", "AWS Region") - // Define Root Flags RootCmd.Flags().BoolVarP(&run.version, "version", "", false, "print current/running version") RootCmd.PersistentFlags().BoolVarP(&run.colors, "no-colors", "", false, "disable colors in outputs") RootCmd.PersistentFlags().StringVarP(&run.profile, "profile", "p", "default", "configured aws profile") + RootCmd.PersistentFlags().StringVarP(&run.region, "region", "r", "", "configured aws region: if blank, the region is acquired via the profile") RootCmd.PersistentFlags().BoolVarP(&run.debug, "debug", "", false, "Run in debug mode...") // Define Lambda Invoke Flags - invokeCmd.Flags().StringVarP(®ion, "region", "r", "eu-west-1", "AWS Region") invokeCmd.Flags().StringVarP(&run.funcEvent, "event", "e", "", "JSON Event data for AWS Lambda invoke") invokeCmd.Flags().BoolVarP(&run.lambdAsync, "async", "x", false, "invoke lambda function asynchronously ") // Define Changes Command changeCmd.AddCommand(create, rm, list, execute, desc) + // Define Protect Command + protectCmd.Flags().BoolVarP(&run.protectOff, "off", "", false, "set termination protection to off") + protectCmd.Flags().BoolVarP(&run.all, "all", "A", false, "protect all stacks") + // Add Config --config common flag for _, cmd := range []interface{}{ checkCmd, @@ -56,9 +62,11 @@ func init() { generateCmd, deployCmd, gitDeployCmd, + gitStatusCmd, policyCmd, valuesCmd, shellCmd, + protectCmd, } { cmd.(*cobra.Command).Flags().StringVarP(&run.cfgSource, "config", "c", defaultConfig(), "path to config file") } @@ -101,8 +109,10 @@ func init() { changeCmd, policyCmd, gitDeployCmd, + gitStatusCmd, valuesCmd, shellCmd, + protectCmd, ) } diff --git a/commands/outputs.go b/commands/outputs.go index a6f7ed7..9ac33f5 100644 --- a/commands/outputs.go +++ b/commands/outputs.go @@ -74,7 +74,7 @@ var ( Example: "qaz exports", PreRun: initialise, Run: func(cmd *cobra.Command, args []string) { - sess, err := manager.GetSess(run.profile) + sess, err := GetSession() utils.HandleError(err) utils.HandleError(stks.Exports(sess)) }, diff --git a/commands/sessions.go b/commands/sessions.go index 5f3e387..d69b6d8 100644 --- a/commands/sessions.go +++ b/commands/sessions.go @@ -6,47 +6,33 @@ import ( "github.com/aws/aws-sdk-go/aws/session" ) -// SessionManager - handles AWS Sessions -type sessionManager struct { - region string - sessions map[string]*session.Session -} - -// GetSess - Returns aws session based on given profile -func (s *sessionManager) GetSess(p string) (*session.Session, error) { +// GetSession - Returns aws session based on default run.profile and run.Region +// options can be overwritten by passing a function: func(*session.Options) +func GetSession(options ...func(*session.Options)) (*session.Session, error) { var sess *session.Session - // Set P to default or command input if stack input is empty - if p == "" { - p = run.profile - } - - if v, ok := s.sessions[p]; ok { - log.Debug("Session Detected: [%s]", p) - return v, nil - } - - options := session.Options{ - Profile: p, + // set default config values + opts := &session.Options{ + Profile: run.profile, SharedConfigState: session.SharedConfigEnable, AssumeRoleTokenProvider: stscreds.StdinTokenProvider, } - if s.region != "" { - options.Config = aws.Config{Region: &s.region} + if run.region != "" { + opts.Config = aws.Config{Region: &run.region} + } + + // apply external Options + for _, f := range options { + f(opts) } - log.Debug("Creating AWS Session with options: Regioin: %s, Profile: %s ", region, run.profile) - sess, err := session.NewSessionWithOptions(options) + log.Debug("Creating AWS Session with options: %s", opts) + sess, err := session.NewSessionWithOptions(*opts) if err != nil { return sess, err } - s.sessions[p] = sess return sess, nil } - -var manager = sessionManager{ - sessions: make(map[string]*session.Session), -} diff --git a/commands/status.go b/commands/status.go index 0e5ad56..16dc9f1 100644 --- a/commands/status.go +++ b/commands/status.go @@ -5,6 +5,7 @@ import ( "strings" "sync" + "github.com/daidokoro/qaz/repo" "github.com/daidokoro/qaz/utils" stks "github.com/daidokoro/qaz/stacks" @@ -40,6 +41,59 @@ var ( }, } + // git-status command + gitStatusCmd = &cobra.Command{ + Use: "git-status [git-repo]", + Short: "Check status of deployment via files stored in Git repository", + Example: "qaz git-status https://github.com/cfn-deployable/simplevpc --user me", + PreRun: initialise, + Run: func(cmd *cobra.Command, args []string) { + + // check args + if len(args) < 1 { + fmt.Println("Please specify git repo...") + return + } + + var wg sync.WaitGroup + + repo, err := repo.NewRepo(args[0], run.gituser, run.gitrsa) + utils.HandleError(err) + + // Passing repo to the global var + gitrepo = *repo + + // add repo + stks.Git = &gitrepo + + if out, ok := repo.Files[run.cfgSource]; ok { + repo.Config = out + } + + log.Debug("Repo Files:") + for k := range repo.Files { + log.Debug(k) + } + + err = Configure(run.cfgSource, repo.Config) + utils.HandleError(err) + + stacks.Range(func(_ string, s *stks.Stack) bool { + wg.Add(1) + go func() { + if err := s.Status(); err != nil { + log.Error("failed to fetch status for [%s]: %v", s.Stackname, err) + } + wg.Done() + }() + return true + }) + + wg.Wait() + + }, + } + // validate/check command checkCmd = &cobra.Command{ Use: "check", @@ -83,4 +137,69 @@ var ( utils.HandleError(stacks.MustGet(s).Check()) }, } + + // protect command + protectCmd = &cobra.Command{ + Use: "protect", + Short: "Enables stack termination protection", + PreRun: initialise, + Example: strings.Join([]string{ + "qaz protect --all", + "qaz protect --all --off", + "qaz protect stack-name --off", + }, "\n"), + Run: func(cmd *cobra.Command, args []string) { + + req := "enabled" + if run.protectOff { + req = "disabled" + } + + if len(args) < 1 && !run.all { + log.Warn("No stack specified for termination") + return + } + + err := Configure(run.cfgSource, "") + utils.HandleError(err) + + var w sync.WaitGroup + + // if run all + if run.all { + stacks.Range(func(_ string, s *stks.Stack) bool { + w.Add(1) + go func() { + defer w.Done() + if err := s.Protect(&run.protectOff); err != nil { + log.Error("error enable termination-protection on: [%s] - %v", s.Name, err) + return + } + log.Info("termination protection %s: [%s]", req, s.Name) + }() + return true + }) + w.Wait() + return + } + + // if individual + for _, s := range args { + if _, ok := stacks.Get(s); !ok { + utils.HandleError(fmt.Errorf("stacks [%s] not found in config", s)) + } + w.Add(1) + go func(s string) { + defer w.Done() + if err := stacks.MustGet(s).Protect(&run.protectOff); err != nil { + log.Error("error enable termination-protection on: [%s] - %v", s, err) + return + } + log.Info("termination protection %s: [%s]", req, s) + }(s) + } + w.Wait() + + }, + } ) diff --git a/commands/vars.go b/commands/vars.go index 0a7fba6..c798e9d 100644 --- a/commands/vars.go +++ b/commands/vars.go @@ -10,7 +10,6 @@ import ( var ( config stks.Config stacks stks.Map - region string project string gitrepo repo.Repo log = logger.Logger{ @@ -32,6 +31,7 @@ var run = struct { cfgSource string tplSource string profile string + region string tplSources []string stacks map[string]string all bool @@ -48,4 +48,5 @@ var run = struct { gituser string gitpass string gitrsa string + protectOff bool }{} diff --git a/commands/version.go b/commands/version.go index 9da563a..979fa77 100644 --- a/commands/version.go +++ b/commands/version.go @@ -1,4 +1,4 @@ package commands // Version -const version = "v0.80-beta" +const version = "v0.81-beta" diff --git a/stacks/clouformation.go b/stacks/clouformation.go index 381d108..19124c6 100644 --- a/stacks/clouformation.go +++ b/stacks/clouformation.go @@ -211,7 +211,9 @@ func TerminateHandler(m *Map) { return true }) - s.terminate() + if err := s.terminate(); err != nil { + Log.Error("error deleting stack: [%s] - %v", s.Name, err) + } return } diff --git a/stacks/config.go b/stacks/config.go index 141d5af..d5f4386 100644 --- a/stacks/config.go +++ b/stacks/config.go @@ -23,6 +23,7 @@ type Config struct { Parameters []map[string]string `yaml:"parameters,omitempty" json:"parameters,omitempty" hcl:"parameters,omitempty"` Policy string `yaml:"policy,omitempty" json:"policy,omitempty" hcl:"policy,omitempty"` Profile string `yaml:"profile,omitempty" json:"profile,omitempty" hcl:"profile,omitempty"` + Region string `yaml:"region,omitempty" json:"region,omitempty" hcl:"region,omitempty"` Source string `yaml:"source,omitempty" json:"source,omitempty" hcl:"source,omitempty"` Bucket string `yaml:"bucket,omitempty" json:"bucket,omitempty" hcl:"bucket,omitempty"` Role string `yaml:"role,omitempty" json:"role,omitempty" hcl:"role,omitempty"` diff --git a/stacks/protect.go b/stacks/protect.go new file mode 100644 index 0000000..07607e2 --- /dev/null +++ b/stacks/protect.go @@ -0,0 +1,23 @@ +package stacks + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudformation" +) + +// Protect - enables stack termination-protection +func (s *Stack) Protect(enable *bool) error { + svc := cloudformation.New(s.Session, &aws.Config{Credentials: s.creds()}) + + params := &cloudformation.UpdateTerminationProtectionInput{ + EnableTerminationProtection: aws.Bool(!*enable), + StackName: &s.Stackname, + } + + Log.Debug("Calling [UpdateTerminationProtection] with parameters:\n%s"+"\n--\n", params) + if _, err := svc.UpdateTerminationProtection(params); err != nil { + return err + } + + return nil +} diff --git a/stacks/services.go b/stacks/services.go index d132865..34a1e49 100644 --- a/stacks/services.go +++ b/stacks/services.go @@ -77,7 +77,7 @@ func tailWait(done <-chan bool, tailinput *TailServiceInput) { for ch := time.Tick(time.Millisecond * 1300); ; <-ch { select { case <-done: - close(tail) + // close(tail) return default: tail <- tailinput diff --git a/stacks/stack.go b/stacks/stack.go index 693c576..e4a1fba 100644 --- a/stacks/stack.go +++ b/stacks/stack.go @@ -45,6 +45,7 @@ type Stack struct { Tags []*cloudformation.Tag Session *session.Session Profile string + Region string Source string Bucket string Role string diff --git a/stacks/terminate.go b/stacks/terminate.go index 7238ff7..afac567 100644 --- a/stacks/terminate.go +++ b/stacks/terminate.go @@ -9,7 +9,7 @@ import ( ) func (s *Stack) terminate() error { - + Log.Debug("terminate called for: [%s]", s.Name) if !s.StackExists() { Log.Info("%s: does not exist...", s.Name) return nil