diff --git a/plugin_test.go b/plugin_test.go index fb57da2..42a2df0 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -1,411 +1,263 @@ package main import ( - "fmt" - "io/ioutil" "os" "os/exec" - "os/user" - "path/filepath" - "regexp" - "strings" - "time" + "testing" - "github.com/Sirupsen/logrus" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/credentials/stscreds" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/sts" + . "github.com/franela/goblin" ) -type ( - // Config holds input parameters for the plugin - Config struct { - Actions []string - Vars map[string]string - Secrets map[string]string - InitOptions InitOptions - FmtOptions FmtOptions - Cacert string - Sensitive bool - RoleARN string - RootDir string - Parallelism int - Targets []string - VarFiles []string - TerraformDataDir string - } - - // Netrc is credentials for cloning - Netrc struct { - Machine string - Login string - Password string - } - - // InitOptions include options for the Terraform's init command - InitOptions struct { - BackendConfig []string `json:"backend-config"` - Lock *bool `json:"lock"` - LockTimeout string `json:"lock-timeout"` - } - - // FmtOptions fmt options for the Terraform's fmt command - FmtOptions struct { - List *bool `json:"list"` - Write *bool `json:"write"` - Diff *bool `json:"diff"` - Check *bool `json:"check"` - } - - // Plugin represents the plugin instance to be executed - Plugin struct { - Config Config - Netrc Netrc - Terraform Terraform - } -) - -// Exec executes the plugin -func (p Plugin) Exec() error { - // Install specified version of terraform - if p.Terraform.Version != "" { - err := installTerraform(p.Terraform.Version) - - if err != nil { - return err - } - } - - if p.Config.RoleARN != "" { - assumeRole(p.Config.RoleARN) - } - - // writing the .netrc file with Github credentials in it. - err := writeNetrc(p.Netrc.Machine, p.Netrc.Login, p.Netrc.Password) - if err != nil { - return err - } - - var terraformDataDir string = ".terraform" - if p.Config.TerraformDataDir != "" { - terraformDataDir = p.Config.TerraformDataDir - os.Setenv("TF_DATA_DIR", p.Config.TerraformDataDir) - } - - var commands []*exec.Cmd - - commands = append(commands, exec.Command("terraform", "version")) - - CopyTfEnv() - - if p.Config.Cacert != "" { - commands = append(commands, installCaCert(p.Config.Cacert)) - } - - commands = append(commands, deleteCache(terraformDataDir)) - commands = append(commands, initCommand(p.Config.InitOptions)) - commands = append(commands, getModules()) - - // Add commands listed from Actions - for _, action := range p.Config.Actions { - switch action { - case "fmt": - commands = append(commands, tfFmt(p.Config)) - case "validate": - commands = append(commands, tfValidate()) - case "plan": - commands = append(commands, tfPlan(p.Config, false)) - case "plan-destroy": - commands = append(commands, tfPlan(p.Config, true)) - case "apply": - commands = append(commands, tfApply(p.Config)) - case "destroy": - commands = append(commands, tfDestroy(p.Config)) - default: - return fmt.Errorf("valid actions are: fmt, validate, plan, apply, plan-destroy, destroy. You provided %s", action) - } - } - - commands = append(commands, deleteCache(terraformDataDir)) - - for _, c := range commands { - if c.Dir == "" { - wd, err := os.Getwd() - if err == nil { - c.Dir = wd +func TestPlugin(t *testing.T) { + g := Goblin(t) + + g.Describe("CopyTfEnv", func() { + g.It("Should create copies of TF_VAR_ to lowercase", func() { + // Set some initial TF_VAR_ that are uppercase + os.Setenv("TF_VAR_SOMETHING", "some value") + os.Setenv("TF_VAR_SOMETHING_ELSE", "some other value") + os.Setenv("TF_VAR_BASE64", "dGVzdA==") + + CopyTfEnv() + + // Make sure new env vars exist with proper values + g.Assert(os.Getenv("TF_VAR_something")).Equal("some value") + g.Assert(os.Getenv("TF_VAR_something_else")).Equal("some other value") + g.Assert(os.Getenv("TF_VAR_base64")).Equal("dGVzdA==") + }) + }) + + g.Describe("tfApply", func() { + g.It("Should return correct apply commands given the arguments", func() { + type args struct { + config Config } - } - if p.Config.RootDir != "" { - c.Dir = c.Dir + "/" + p.Config.RootDir - } - c.Stdout = os.Stdout - c.Stderr = os.Stderr - if !p.Config.Sensitive { - trace(c) - } - - err := c.Run() - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Failed to execute a command") - } - logrus.Debug("Command completed successfully") - } - - return nil -} - -// CopyTfEnv creates copies of TF_VAR_ to lowercase -func CopyTfEnv() { - tfVar := regexp.MustCompile(`^TF_VAR_.*$`) - for _, e := range os.Environ() { - pair := strings.SplitN(e, "=", 2) - if tfVar.MatchString(pair[0]) { - name := strings.Split(pair[0], "TF_VAR_") - os.Setenv(fmt.Sprintf("TF_VAR_%s", strings.ToLower(name[1])), pair[1]) - } - } -} - -func assumeRole(roleArn string) { - client := sts.New(session.New()) - duration := time.Hour * 1 - stsProvider := &stscreds.AssumeRoleProvider{ - Client: client, - Duration: duration, - RoleARN: roleArn, - RoleSessionName: "drone", - } - - value, err := credentials.NewCredentials(stsProvider).Get() - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Error assuming role!") - } - os.Setenv("AWS_ACCESS_KEY_ID", value.AccessKeyID) - os.Setenv("AWS_SECRET_ACCESS_KEY", value.SecretAccessKey) - os.Setenv("AWS_SESSION_TOKEN", value.SessionToken) -} - -func deleteCache(terraformDataDir string) *exec.Cmd { - return exec.Command( - "rm", - "-rf", - terraformDataDir, - ) -} - -func getModules() *exec.Cmd { - return exec.Command( - "terraform", - "get", - ) -} - -func initCommand(config InitOptions) *exec.Cmd { - args := []string{ - "init", - } - - for _, v := range config.BackendConfig { - args = append(args, fmt.Sprintf("-backend-config=%s", v)) - } - // True is default in TF - if config.Lock != nil { - args = append(args, fmt.Sprintf("-lock=%t", *config.Lock)) - } - - // "0s" is default in TF - if config.LockTimeout != "" { - args = append(args, fmt.Sprintf("-lock-timeout=%s", config.LockTimeout)) - } - - // Fail Terraform execution on prompt - args = append(args, "-input=false") - - return exec.Command( - "terraform", - args..., - ) -} + tests := []struct { + name string + args args + want *exec.Cmd + }{ + { + "default", + args{config: Config{}}, + exec.Command("terraform", "apply", "plan.tfout"), + }, + { + "with parallelism", + args{config: Config{Parallelism: 5}}, + exec.Command("terraform", "apply", "-parallelism=5", "plan.tfout"), + }, + { + "with targets", + args{config: Config{Targets: []string{"target1", "target2"}}}, + exec.Command("terraform", "apply", "--target", "target1", "--target", "target2", "plan.tfout"), + }, + } -func installCaCert(cacert string) *exec.Cmd { - ioutil.WriteFile("/usr/local/share/ca-certificates/ca_cert.crt", []byte(cacert), 0644) - return exec.Command( - "update-ca-certificates", - ) -} + for _, tt := range tests { + g.Assert(tfApply(tt.args.config)).Equal(tt.want) + } + }) + }) -func trace(cmd *exec.Cmd) { - fmt.Println("$", strings.Join(cmd.Args, " ")) -} + g.Describe("tfDestroy", func() { + g.It("Should return correct destroy commands given the arguments", func() { + type args struct { + config Config + } -func tfApply(config Config) *exec.Cmd { - args := []string{ - "apply", - } - for _, v := range config.Targets { - args = append(args, "--target", fmt.Sprintf("%s", v)) - } - if config.Parallelism > 0 { - args = append(args, fmt.Sprintf("-parallelism=%d", config.Parallelism)) - } - if config.InitOptions.Lock != nil { - args = append(args, fmt.Sprintf("-lock=%t", *config.InitOptions.Lock)) - } - if config.InitOptions.LockTimeout != "" { - args = append(args, fmt.Sprintf("-lock-timeout=%s", config.InitOptions.LockTimeout)) - } - args = append(args, getTfoutPath()) + tests := []struct { + name string + args args + want *exec.Cmd + }{ + { + "default", + args{config: Config{}}, + exec.Command("terraform", "destroy", "-force"), + }, + { + "with parallelism", + args{config: Config{Parallelism: 5}}, + exec.Command("terraform", "destroy", "-parallelism=5", "-force"), + }, + { + "with targets", + args{config: Config{Targets: []string{"target1", "target2"}}}, + exec.Command("terraform", "destroy", "-target=target1", "-target=target2", "-force"), + }, + { + "with vars", + args{config: Config{Vars: map[string]string{"username": "someuser", "password": "1pass"}}}, + exec.Command("terraform", "destroy", "-var", "username=someuser", "-var", "password=1pass", "-force"), + }, + { + "with var-files", + args{config: Config{VarFiles: []string{"common.tfvars", "prod.tfvars"}}}, + exec.Command("terraform", "destroy", "-var-file=common.tfvars", "-var-file=prod.tfvars", "-force"), + }, + } - return exec.Command( - "terraform", - args..., - ) -} + for _, tt := range tests { + g.Assert(tfDestroy(tt.args.config)).Equal(tt.want) + } + }) + }) + + g.Describe("tfValidate", func() { + g.It("Should return correct validate command", func() { + tests := []struct { + name string + want *exec.Cmd + }{ + { + "default", + exec.Command("terraform", "validate"), + }, + } -func tfDestroy(config Config) *exec.Cmd { - args := []string{ - "destroy", - } - for _, v := range config.Targets { - args = append(args, fmt.Sprintf("-target=%s", v)) - } - args = append(args, varFiles(config.VarFiles)...) - args = append(args, vars(config.Vars)...) - if config.Parallelism > 0 { - args = append(args, fmt.Sprintf("-parallelism=%d", config.Parallelism)) - } - if config.InitOptions.Lock != nil { - args = append(args, fmt.Sprintf("-lock=%t", *config.InitOptions.Lock)) - } - if config.InitOptions.LockTimeout != "" { - args = append(args, fmt.Sprintf("-lock-timeout=%s", config.InitOptions.LockTimeout)) - } - args = append(args, "-force") - return exec.Command( - "terraform", - args..., - ) -} + for _, tt := range tests { + g.Assert(tfValidate()).Equal(tt.want) + } + }) + }) -func tfPlan(config Config, destroy bool) *exec.Cmd { - args := []string{ - "plan", - } + g.Describe("tfPlan", func() { + g.It("Should return correct plan commands given the arguments", func() { + type args struct { + config Config + } - if destroy { - args = append(args, "-destroy") - } else { - args = append(args, fmt.Sprintf("-out=%s", getTfoutPath())) - } + tests := []struct { + name string + args args + destroy bool + want *exec.Cmd + }{ + { + "default", + args{config: Config{}}, + false, + exec.Command("terraform", "plan", "-out=plan.tfout"), + }, + { + "destroy", + args{config: Config{}}, + true, + exec.Command("terraform", "plan", "-destroy"), + }, + { + "with vars", + args{config: Config{Vars: map[string]string{"username": "someuser", "password": "1pass"}}}, + false, + exec.Command("terraform", "plan", "-out=plan.tfout", "-var", "username=someuser", "-var", "password=1pass"), + }, + { + "with var-files", + args{config: Config{VarFiles: []string{"common.tfvars", "prod.tfvars"}}}, + false, + exec.Command("terraform", "plan", "-out=plan.tfout", "-var-file=common.tfvars", "-var-file=prod.tfvars"), + }, + } - for _, v := range config.Targets { - args = append(args, "--target", fmt.Sprintf("%s", v)) - } - args = append(args, varFiles(config.VarFiles)...) - args = append(args, vars(config.Vars)...) - if config.Parallelism > 0 { - args = append(args, fmt.Sprintf("-parallelism=%d", config.Parallelism)) - } - if config.InitOptions.Lock != nil { - args = append(args, fmt.Sprintf("-lock=%t", *config.InitOptions.Lock)) - } - if config.InitOptions.LockTimeout != "" { - args = append(args, fmt.Sprintf("-lock-timeout=%s", config.InitOptions.LockTimeout)) - } - return exec.Command( - "terraform", - args..., - ) -} + for _, tt := range tests { + g.Assert(tfPlan(tt.args.config, tt.destroy)).Equal(tt.want) + } + }) + }) + g.Describe("tfFmt", func() { + g.It("Should return correct fmt commands given the arguments", func() { + type args struct { + config Config + } -func tfValidate() *exec.Cmd { - args := []string{ - "validate", - } - return exec.Command( - "terraform", - args..., - ) -} + affirmative := true + negative := false + + tests := []struct { + name string + args args + want *exec.Cmd + }{ + { + "default", + args{config: Config{}}, + exec.Command("terraform", "fmt"), + }, + { + "with list", + args{config: Config{FmtOptions: FmtOptions{List: &affirmative}}}, + exec.Command("terraform", "fmt", "-list=true"), + }, + { + "with write", + args{config: Config{FmtOptions: FmtOptions{Write: &affirmative}}}, + exec.Command("terraform", "fmt", "-write=true"), + }, + { + "with diff", + args{config: Config{FmtOptions: FmtOptions{Diff: &affirmative}}}, + exec.Command("terraform", "fmt", "-diff=true"), + }, + { + "with check", + args{config: Config{FmtOptions: FmtOptions{Check: &affirmative}}}, + exec.Command("terraform", "fmt", "-check=true"), + }, + { + "with combination", + args{config: Config{FmtOptions: FmtOptions{ + List: &negative, + Write: &negative, + Diff: &affirmative, + Check: &affirmative, + }}}, + exec.Command("terraform", "fmt", "-list=false", "-write=false", "-diff=true", "-check=true"), + }, + } -func tfFmt(config Config) *exec.Cmd { - args := []string{ - "fmt", - } - if config.FmtOptions.List != nil { - args = append(args, fmt.Sprintf("-list=%t", *config.FmtOptions.List)) - } - if config.FmtOptions.Write != nil { - args = append(args, fmt.Sprintf("-write=%t", *config.FmtOptions.Write)) - } - if config.FmtOptions.Diff != nil { - args = append(args, fmt.Sprintf("-diff=%t", *config.FmtOptions.Diff)) - } - if config.FmtOptions.Check != nil { - args = append(args, fmt.Sprintf("-check=%t", *config.FmtOptions.Check)) - } - return exec.Command( - "terraform", - args..., - ) -} + for _, tt := range tests { + g.Assert(tfFmt(tt.args.config)).Equal(tt.want) + } + }) + }) -func getTfoutPath() string { - terraformDataDir := os.Getenv("TF_DATA_DIR") - if terraformDataDir == ".terraform" || terraformDataDir == "" { - return "plan.tfout" - } else { - return fmt.Sprintf("%s.plan.tfout", terraformDataDir) - } -} + g.Describe("tfDataDir", func() { + g.It("Should override the terraform data dir environment variable when provided", func() { + type args struct { + config Config + } -func vars(vs map[string]string) []string { - var args []string - for k, v := range vs { - args = append(args, "-var", fmt.Sprintf("%s=%s", k, v)) - } - return args -} + tests := []struct { + name string + args args + want *exec.Cmd + }{ + { + "with TerraformDataDir", + args{config: Config{TerraformDataDir: ".overriden_terraform_dir"}}, + exec.Command("terraform", "apply", ".overriden_terraform_dir.plan.tfout"), + }, + { + "with TerraformDataDir value as .terraform", + args{config: Config{TerraformDataDir: ".terraform"}}, + exec.Command("terraform", "apply", "plan.tfout"), + }, + { + "without TerraformDataDir", + args{config: Config{}}, + exec.Command("terraform", "apply", "plan.tfout"), + }, + } -func varFiles(vfs []string) []string { - var args []string - for _, v := range vfs { - args = append(args, fmt.Sprintf("-var-file=%s", v)) - } - return args -} + for _, tt := range tests { + os.Setenv("TF_DATA_DIR", tt.args.config.TerraformDataDir) + applied := tfApply(tt.args.config) -// helper function to write a netrc file. -// The following code comes from the official Git plugin for Drone: -// https://github.com/drone-plugins/drone-git/blob/8386effd2fe8c8695cf979427f8e1762bd805192/utils.go#L43-L68 -func writeNetrc(machine, login, password string) error { - if machine == "" { - return nil - } - out := fmt.Sprintf( - netrcFile, - machine, - login, - password, - ) + g.Assert(applied).Equal(tt.want) - home := "/root" - u, err := user.Current() - if err == nil { - home = u.HomeDir - } - path := filepath.Join(home, ".netrc") - return ioutil.WriteFile(path, []byte(out), 0600) + } + }) + }) } - -const netrcFile = ` -machine %s -login %s -password %s -`