From c4ef8ee8478e6654a94fbeae673b9d8571314a7f Mon Sep 17 00:00:00 2001 From: daidokoro Date: Mon, 13 Mar 2017 01:05:46 +0000 Subject: [PATCH] Implemented Change-Set Management. Split-out stacks type to separate file for easier expansion. Version bump --- change.go | 230 ++++++++++++++++++++++++++++++ cloudformation.go | 219 ----------------------------- commands.go | 10 ++ init.go | 20 ++- main.go | 2 +- stack.go | 346 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 605 insertions(+), 222 deletions(-) create mode 100644 change.go create mode 100644 stack.go diff --git a/change.go b/change.go new file mode 100644 index 0000000..21fd045 --- /dev/null +++ b/change.go @@ -0,0 +1,230 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var changeCmd = &cobra.Command{ + Use: "change", + Short: "Change-Set management for AWS Stacks", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + + }, +} + +var create = &cobra.Command{ + Use: "create", + Short: "Create Changet-Set", + Run: func(cmd *cobra.Command, args []string) { + job.request = "change-set create" + + if len(args) < 1 { + fmt.Println("Please provide Change-Set Name...") + return + } + + job.changeName = args[0] + + s, source, err := getSource(job.tplFile) + if err != nil { + handleError(err) + return + } + + job.tplFile = source + + err = configReader(job.cfgFile) + if err != nil { + handleError(err) + return + } + + v, err := genTimeParser(job.tplFile) + if err != nil { + handleError(err) + return + } + + // Handle missing stacks + if stacks[s] == nil { + handleError(fmt.Errorf("Missing Stack in %s: [%s]", job.cfgFile, s)) + return + } + + stacks[s].template = v + + // resolve deploy time function + if err = stacks[s].deployTimeParser(); err != nil { + handleError(err) + } + + // create session + sess, err := awsSession() + if err != nil { + handleError(err) + return + } + + if err := stacks[s].change(sess, "create"); err != nil { + handleError(err) + } + + }, +} + +var rm = &cobra.Command{ + Use: "rm", + Short: "Delete Change-Set", + Run: func(cmd *cobra.Command, args []string) { + job.request = "change-set delete" + + if len(args) < 1 { + fmt.Println("Please provide Change-Set Name...") + return + } + + if job.stackName == "" { + fmt.Println("Please specify stack name using --stack OR -s ...") + return + } + + job.changeName = args[0] + + err := configReader(job.cfgFile) + if err != nil { + handleError(err) + return + } + + s := &stack{name: job.stackName} + s.setStackName() + + // create session + sess, err := awsSession() + if err != nil { + handleError(err) + return + } + + if err := s.change(sess, "rm"); err != nil { + handleError(err) + } + + }, +} + +var list = &cobra.Command{ + Use: "list", + Short: "List Change-Sets", + Run: func(cmd *cobra.Command, args []string) { + job.request = "change-set list" + + if job.stackName == "" { + fmt.Println("Please specify stack name using --stack OR -s ...") + return + } + + err := configReader(job.cfgFile) + if err != nil { + handleError(err) + return + } + + s := &stack{name: job.stackName} + s.setStackName() + + // create session + sess, err := awsSession() + if err != nil { + handleError(err) + return + } + + if err := s.change(sess, "list"); err != nil { + handleError(err) + } + }, +} + +var execute = &cobra.Command{ + Use: "execute", + Short: "Execute Change-Set", + Run: func(cmd *cobra.Command, args []string) { + job.request = "change-set execute" + + if len(args) < 1 { + fmt.Println("Please provide Change-Set Name...") + return + } + + if job.stackName == "" { + fmt.Println("Please specify stack name using --stack OR -s ...") + return + } + + job.changeName = args[0] + + err := configReader(job.cfgFile) + if err != nil { + handleError(err) + return + } + + s := &stack{name: job.stackName} + s.setStackName() + + // create session + sess, err := awsSession() + if err != nil { + handleError(err) + return + } + + if err := s.change(sess, "execute"); err != nil { + handleError(err) + } + }, +} + +var desc = &cobra.Command{ + Use: "desc", + Short: "Describe Change-Set", + Run: func(cmd *cobra.Command, args []string) { + job.request = "change-set decribe" + + if len(args) < 1 { + fmt.Println("Please provide Change-Set Name...") + return + } + + if job.stackName == "" { + fmt.Println("Please specify stack name using --stack OR -s ...") + return + } + + job.changeName = args[0] + + err := configReader(job.cfgFile) + if err != nil { + handleError(err) + return + } + + s := &stack{name: job.stackName} + s.setStackName() + + // create session + sess, err := awsSession() + if err != nil { + handleError(err) + return + } + + if err := s.change(sess, "desc"); err != nil { + handleError(err) + } + }, +} diff --git a/cloudformation.go b/cloudformation.go index 966d5d2..ec0029a 100644 --- a/cloudformation.go +++ b/cloudformation.go @@ -3,7 +3,6 @@ package main import ( "errors" "fmt" - "strings" "sync" "time" @@ -12,17 +11,6 @@ import ( "github.com/aws/aws-sdk-go/service/cloudformation" ) -// stack - holds all meaningful information about a particular stack. -type stack struct { - name string - stackname string - template string - dependsOn []interface{} - dependents []interface{} - stackoutputs *cloudformation.DescribeStacksOutput - parameters []*cloudformation.Parameter -} - // State - struct for handling stack deploy/terminate statuses var state = struct { pending string @@ -45,188 +33,6 @@ func updateState(statusMap map[string]string, name string, status string) { mutex.Unlock() } -// setStackName - sets the stackname with struct -func (s *stack) setStackName() { - s.stackname = fmt.Sprintf("%s-%s", project, s.name) -} - -func (s *stack) deploy(session *session.Session) error { - - err := s.deployTimeParser() - if err != nil { - return err - } - - Log(fmt.Sprintf("Updated Template:\n%s", s.template), level.debug) - - svc := cloudformation.New(session) - - createParams := &cloudformation.CreateStackInput{ - StackName: aws.String(s.stackname), - DisableRollback: aws.Bool(true), // no rollback by default - TemplateBody: aws.String(s.template), - } - - // NOTE: Add parameters flag here if params set - if len(s.parameters) > 0 { - createParams.Parameters = s.parameters - } - - // If IAM is bening touched, add Capabilities - if strings.Contains(s.template, "AWS::IAM") { - createParams.Capabilities = []*string{ - aws.String(cloudformation.CapabilityCapabilityIam), - } - } - - Log(fmt.Sprintln("Calling [CreateStack] with parameters:", createParams), level.debug) - if _, err := svc.CreateStack(createParams); err != nil { - return errors.New(fmt.Sprintln("Deploying failed: ", err.Error())) - - } - - go verbose(s.stackname, "CREATE", session) - describeStacksInput := &cloudformation.DescribeStacksInput{ - StackName: aws.String(s.stackname), - } - - Log(fmt.Sprintln("Calling [WaitUntilStackCreateComplete] with parameters:", describeStacksInput), level.debug) - if err := svc.WaitUntilStackCreateComplete(describeStacksInput); err != nil { - return err - } - - Log(fmt.Sprintf("Deployment successful: [%s]", s.stackname), "info") - - return nil -} - -func (s *stack) update(session *session.Session) error { - svc := cloudformation.New(session) - capability := "CAPABILITY_IAM" - updateParams := &cloudformation.UpdateStackInput{ - StackName: aws.String(s.stackname), - TemplateBody: aws.String(s.template), - Capabilities: []*string{&capability}, - } - - // NOTE: Add parameters flag here if params set - if len(s.parameters) > 0 { - updateParams.Parameters = s.parameters - } - - if s.stackExists(session) { - Log("Stack exists, updating...", "info") - - Log(fmt.Sprintln("Calling [UpdateStack] with parameters:", updateParams), level.debug) - _, err := svc.UpdateStack(updateParams) - - if err != nil { - return errors.New(fmt.Sprintln("Update failed: ", err)) - } - - go verbose(s.stackname, "UPDATE", session) - - describeStacksInput := &cloudformation.DescribeStacksInput{ - StackName: aws.String(s.stackname), - } - Log(fmt.Sprintln("Calling [WaitUntilStackUpdateComplete] with parameters:", describeStacksInput), level.debug) - if err := svc.WaitUntilStackUpdateComplete(describeStacksInput); err != nil { - return err - } - - Log(fmt.Sprintf("Stack update successful: [%s]", s.stackname), "info") - - } - return nil -} - -func (s *stack) terminate(session *session.Session) error { - svc := cloudformation.New(session) - - params := &cloudformation.DeleteStackInput{ - StackName: aws.String(s.stackname), - } - - Log(fmt.Sprintln("Calling [DeleteStack] with parameters:", params), level.debug) - _, err := svc.DeleteStack(params) - - go verbose(s.stackname, "DELETE", session) - - if err != nil { - return errors.New(fmt.Sprintln("Deleting failed: ", err)) - } - - describeStacksInput := &cloudformation.DescribeStacksInput{ - StackName: aws.String(s.stackname), - } - - Log(fmt.Sprintln("Calling [WaitUntilStackDeleteComplete] with parameters:", describeStacksInput), level.debug) - if err := svc.WaitUntilStackDeleteComplete(describeStacksInput); err != nil { - return err - } - - Log(fmt.Sprintf("Deletion successful: [%s]", s.stackname), "info") - return nil -} - -func (s *stack) stackExists(session *session.Session) bool { - svc := cloudformation.New(session) - - describeStacksInput := &cloudformation.DescribeStacksInput{ - StackName: aws.String(s.stackname), - } - - Log(fmt.Sprintln("Calling [DescribeStacks] with parameters:", describeStacksInput), level.debug) - _, err := svc.DescribeStacks(describeStacksInput) - - if err == nil { - return true - } - - return false -} - -func (s *stack) status(session *session.Session) error { - svc := cloudformation.New(session) - - describeStacksInput := &cloudformation.DescribeStacksInput{ - StackName: aws.String(s.stackname), - } - - Log(fmt.Sprintln("Calling [UpdateStack] with parameters:", describeStacksInput), level.debug) - status, err := svc.DescribeStacks(describeStacksInput) - - if err != nil { - if strings.Contains(strings.ToLower(err.Error()), "exist") { - fmt.Printf("create_pending -> %s [%s]"+"\n", s.name, s.stackname) - return nil - } - return err - } - - // Define time flag - stat := *status.Stacks[0].StackStatus - var timeflag time.Time - switch strings.Split(stat, "_")[0] { - case "UPDATE": - timeflag = *status.Stacks[0].LastUpdatedTime - default: - timeflag = *status.Stacks[0].CreationTime - } - - // Print Status - fmt.Printf( - "%s%s - %s --> %s - [%s]"+"\n", - colorString(`@`, "magenta"), - timeflag.Format(time.RFC850), - strings.ToLower(colorMap(*status.Stacks[0].StackStatus)), - s.name, - s.stackname, - ) - - return nil -} - // StackOutputs - Returns outputs of given stackname func StackOutputs(name string, session *session.Session) (*cloudformation.DescribeStacksOutput, error) { @@ -266,30 +72,6 @@ func Exports(session *session.Session) error { return nil } -func (s *stack) state(session *session.Session) (string, error) { - svc := cloudformation.New(session) - - describeStacksInput := &cloudformation.DescribeStacksInput{ - StackName: aws.String(s.stackname), - } - - Log(fmt.Sprintln("Calling [DescribeStacks] with parameters: ", describeStacksInput), level.debug) - status, err := svc.DescribeStacks(describeStacksInput) - if err != nil { - if strings.Contains(err.Error(), "not exist") { - return state.pending, nil - } - return "", err - } - - if strings.Contains(strings.ToLower(status.GoString()), "complete") { - return state.complete, nil - } else if strings.Contains(strings.ToLower(status.GoString()), "fail") { - return state.failed, nil - } - return "", nil -} - // Check - Validates Cloudformation Templates func Check(template string, session *session.Session) error { svc := cloudformation.New(session) @@ -310,7 +92,6 @@ func Check(template string, session *session.Session) error { ) return nil - } // DeployHandler - Handles deploying stacks in the corrcet order diff --git a/commands.go b/commands.go index 590faa1..58e5a00 100644 --- a/commands.go +++ b/commands.go @@ -25,6 +25,8 @@ var job = struct { request string debug bool funcEvent string + changeName string + stackName string }{} // Wait Group for handling goroutines @@ -507,3 +509,11 @@ var tailCmd = &cobra.Command{ wg.Wait() // Will probably wait forevery }, } + +var costCmd = &cobra.Command{ + Use: "cost", + Short: "Estimate stack/project Cost based on templates", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(colorString("Coming Soon....", "magenta")) + }, +} diff --git a/init.go b/init.go index 7ffe348..bb33c24 100644 --- a/init.go +++ b/init.go @@ -4,6 +4,7 @@ package main import ( log "github.com/Sirupsen/logrus" + "github.com/spf13/cobra" prefixed "github.com/x-cray/logrus-prefixed-formatter" ) @@ -90,13 +91,28 @@ func init() { // Define Invoke Flags invokeCmd.Flags().StringVarP(®ion, "region", "r", "eu-west-1", "AWS Region") - invokeCmd.Flags().StringVarP(&job.funcEvent, "event", "e", "", "Lambda JSON event Data") + + // Define Changes Command + changeCmd.AddCommand( + create, rm, + list, execute, + desc, + ) + + for _, cmd := range []interface{}{create, list, rm, execute, desc} { + cmd.(*cobra.Command).Flags().StringVarP(&job.cfgFile, "config", "c", "config.yml", "path to config file [Required]") + cmd.(*cobra.Command).Flags().StringVarP(&job.stackName, "stack", "s", "", "Qaz local project Stack Name [Required]") + } + + create.Flags().StringVarP(&job.tplFile, "template", "t", "template", "path to template file Or stack::url") + changeCmd.Flags().StringVarP(&job.cfgFile, "config", "c", "config.yml", "path to config file") rootCmd.AddCommand( generateCmd, deployCmd, terminateCmd, statusCmd, outputsCmd, initCmd, updateCmd, checkCmd, exportsCmd, - invokeCmd, tailCmd, + invokeCmd, tailCmd, changeCmd, + costCmd, ) // Setup logging diff --git a/main.go b/main.go index 042a4fa..d6df9dc 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,7 @@ import ( ) // Version -const version = "v0.35.7-alpha" +const version = "v0.36.7-alpha" func main() { if err := rootCmd.Execute(); err != nil { diff --git a/stack.go b/stack.go new file mode 100644 index 0000000..e25cfd3 --- /dev/null +++ b/stack.go @@ -0,0 +1,346 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudformation" +) + +// stack - holds all meaningful information about a particular stack. +type stack struct { + name string + stackname string + template string + dependsOn []interface{} + dependents []interface{} + stackoutputs *cloudformation.DescribeStacksOutput + parameters []*cloudformation.Parameter +} + +// setStackName - sets the stackname with struct +func (s *stack) setStackName() { + s.stackname = fmt.Sprintf("%s-%s", project, s.name) +} + +func (s *stack) deploy(session *session.Session) error { + + err := s.deployTimeParser() + if err != nil { + return err + } + + Log(fmt.Sprintf("Updated Template:\n%s", s.template), level.debug) + + svc := cloudformation.New(session) + + createParams := &cloudformation.CreateStackInput{ + StackName: aws.String(s.stackname), + DisableRollback: aws.Bool(true), // no rollback by default + TemplateBody: aws.String(s.template), + } + + // NOTE: Add parameters flag here if params set + if len(s.parameters) > 0 { + createParams.Parameters = s.parameters + } + + // If IAM is bening touched, add Capabilities + if strings.Contains(s.template, "AWS::IAM") { + createParams.Capabilities = []*string{ + aws.String(cloudformation.CapabilityCapabilityIam), + } + } + + Log(fmt.Sprintln("Calling [CreateStack] with parameters:", createParams), level.debug) + if _, err := svc.CreateStack(createParams); err != nil { + return errors.New(fmt.Sprintln("Deploying failed: ", err.Error())) + + } + + go verbose(s.stackname, "CREATE", session) + describeStacksInput := &cloudformation.DescribeStacksInput{ + StackName: aws.String(s.stackname), + } + + Log(fmt.Sprintln("Calling [WaitUntilStackCreateComplete] with parameters:", describeStacksInput), level.debug) + if err := svc.WaitUntilStackCreateComplete(describeStacksInput); err != nil { + return err + } + + Log(fmt.Sprintf("Deployment successful: [%s]", s.stackname), "info") + + return nil +} + +func (s *stack) update(session *session.Session) error { + svc := cloudformation.New(session) + capability := "CAPABILITY_IAM" + updateParams := &cloudformation.UpdateStackInput{ + StackName: aws.String(s.stackname), + TemplateBody: aws.String(s.template), + Capabilities: []*string{&capability}, + } + + // NOTE: Add parameters flag here if params set + if len(s.parameters) > 0 { + updateParams.Parameters = s.parameters + } + + if s.stackExists(session) { + Log("Stack exists, updating...", "info") + + Log(fmt.Sprintln("Calling [UpdateStack] with parameters:", updateParams), level.debug) + _, err := svc.UpdateStack(updateParams) + + if err != nil { + return errors.New(fmt.Sprintln("Update failed: ", err)) + } + + go verbose(s.stackname, "UPDATE", session) + + describeStacksInput := &cloudformation.DescribeStacksInput{ + StackName: aws.String(s.stackname), + } + Log(fmt.Sprintln("Calling [WaitUntilStackUpdateComplete] with parameters:", describeStacksInput), level.debug) + if err := svc.WaitUntilStackUpdateComplete(describeStacksInput); err != nil { + return err + } + + Log(fmt.Sprintf("Stack update successful: [%s]", s.stackname), "info") + + } + return nil +} + +func (s *stack) terminate(session *session.Session) error { + svc := cloudformation.New(session) + + params := &cloudformation.DeleteStackInput{ + StackName: aws.String(s.stackname), + } + + Log(fmt.Sprintln("Calling [DeleteStack] with parameters:", params), level.debug) + _, err := svc.DeleteStack(params) + + go verbose(s.stackname, "DELETE", session) + + if err != nil { + return errors.New(fmt.Sprintln("Deleting failed: ", err)) + } + + describeStacksInput := &cloudformation.DescribeStacksInput{ + StackName: aws.String(s.stackname), + } + + Log(fmt.Sprintln("Calling [WaitUntilStackDeleteComplete] with parameters:", describeStacksInput), level.debug) + if err := svc.WaitUntilStackDeleteComplete(describeStacksInput); err != nil { + return err + } + + Log(fmt.Sprintf("Deletion successful: [%s]", s.stackname), "info") + return nil +} + +func (s *stack) stackExists(session *session.Session) bool { + svc := cloudformation.New(session) + + describeStacksInput := &cloudformation.DescribeStacksInput{ + StackName: aws.String(s.stackname), + } + + Log(fmt.Sprintln("Calling [DescribeStacks] with parameters:", describeStacksInput), level.debug) + _, err := svc.DescribeStacks(describeStacksInput) + + if err == nil { + return true + } + + return false +} + +func (s *stack) status(session *session.Session) error { + svc := cloudformation.New(session) + + describeStacksInput := &cloudformation.DescribeStacksInput{ + StackName: aws.String(s.stackname), + } + + Log(fmt.Sprintln("Calling [UpdateStack] with parameters:", describeStacksInput), level.debug) + status, err := svc.DescribeStacks(describeStacksInput) + + if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "exist") { + fmt.Printf("create_pending -> %s [%s]"+"\n", s.name, s.stackname) + return nil + } + return err + } + + // Define time flag + stat := *status.Stacks[0].StackStatus + var timeflag time.Time + switch strings.Split(stat, "_")[0] { + case "UPDATE": + timeflag = *status.Stacks[0].LastUpdatedTime + default: + timeflag = *status.Stacks[0].CreationTime + } + + // Print Status + fmt.Printf( + "%s%s - %s --> %s - [%s]"+"\n", + colorString(`@`, "magenta"), + timeflag.Format(time.RFC850), + strings.ToLower(colorMap(*status.Stacks[0].StackStatus)), + s.name, + s.stackname, + ) + + return nil +} + +func (s *stack) state(session *session.Session) (string, error) { + svc := cloudformation.New(session) + + describeStacksInput := &cloudformation.DescribeStacksInput{ + StackName: aws.String(s.stackname), + } + + Log(fmt.Sprintln("Calling [DescribeStacks] with parameters: ", describeStacksInput), level.debug) + status, err := svc.DescribeStacks(describeStacksInput) + if err != nil { + if strings.Contains(err.Error(), "not exist") { + return state.pending, nil + } + return "", err + } + + if strings.Contains(strings.ToLower(status.GoString()), "complete") { + return state.complete, nil + } else if strings.Contains(strings.ToLower(status.GoString()), "fail") { + return state.failed, nil + } + return "", nil +} + +func (s *stack) change(session *session.Session, req string) error { + svc := cloudformation.New(session) + + switch req { + + case "create": + // Resolve Deploy-Time functions + err := s.deployTimeParser() + if err != nil { + return err + } + + params := &cloudformation.CreateChangeSetInput{ + StackName: aws.String(s.stackname), + ChangeSetName: aws.String(job.changeName), + } + + Log(fmt.Sprintf("Updated Template:\n%s", s.template), level.debug) + + params.TemplateBody = aws.String(s.template) + if _, err = svc.CreateChangeSet(params); err != nil { + return err + } + + describeParams := &cloudformation.DescribeChangeSetInput{ + StackName: aws.String(s.stackname), + ChangeSetName: aws.String(job.changeName), + } + + for { + // Waiting for PENDING state to change + resp, err := svc.DescribeChangeSet(describeParams) + if err != nil { + return err + } + + Log(fmt.Sprintf("Creating Change-Set: [%s] - %s - %s", job.changeName, colorMap(*resp.Status), s.stackname), level.info) + + if *resp.Status == "CREATE_COMPLETE" || *resp.Status == "FAILED" { + break + } + + time.Sleep(time.Second * 1) + } + + case "rm": + params := &cloudformation.DeleteChangeSetInput{ + ChangeSetName: aws.String(job.changeName), + StackName: aws.String(s.stackname), + } + + if _, err := svc.DeleteChangeSet(params); err != nil { + return err + } + + Log(fmt.Sprintf("Change-Set: [%s] deleted", job.changeName), level.info) + + case "list": + params := &cloudformation.ListChangeSetsInput{ + StackName: aws.String(s.stackname), + } + + resp, err := svc.ListChangeSets(params) + if err != nil { + return err + } + + // if strings.Contains(resp.GoString(), "Summaries:") { + for _, i := range resp.Summaries { + Log(fmt.Sprintf("%s%s - Change-Set: [%s] - Status: [%s]", colorString("@", "magenta"), i.CreationTime.Format(time.RFC850), *i.ChangeSetName, *i.ExecutionStatus), level.info) + } + // } + + case "execute": + params := &cloudformation.ExecuteChangeSetInput{ + StackName: aws.String(s.stackname), + ChangeSetName: aws.String(job.changeName), + } + + if _, err := svc.ExecuteChangeSet(params); err != nil { + return err + } + + describeStacksInput := &cloudformation.DescribeStacksInput{ + StackName: aws.String(s.stackname), + } + + go verbose(s.stackname, "UPDATE", session) + + Log(fmt.Sprintln("Calling [WaitUntilStackUpdateComplete] with parameters:", describeStacksInput), level.debug) + if err := svc.WaitUntilStackUpdateComplete(describeStacksInput); err != nil { + return err + } + + case "desc": + params := &cloudformation.DescribeChangeSetInput{ + ChangeSetName: aws.String(job.changeName), + StackName: aws.String(s.stackname), + } + + resp, err := svc.DescribeChangeSet(params) + if err != nil { + return err + } + + o, err := json.MarshalIndent(resp, "", " ") + if err != nil { + return err + } + + fmt.Printf("%s\n", o) + } + + return nil +}