diff --git a/.drone.yml b/.drone.yml index 49045d3..94354d8 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,7 +4,7 @@ workspace: pipeline: test: - image: golang:1.8 + image: golang:1.9 environment: - CGO_ENABLED=0 commands: diff --git a/main.go b/main.go index fac697c..ce00fba 100644 --- a/main.go +++ b/main.go @@ -23,35 +23,30 @@ func main() { // plugin args // - cli.BoolFlag{ - Name: "plan", - Usage: "calculates a plan but does NOT apply it", - EnvVar: "PLUGIN_PLAN", - }, - cli.StringFlag{ - Name: "init_options", - Usage: "options for the init command. See https://www.terraform.io/docs/commands/init.html", - EnvVar: "PLUGIN_INIT_OPTIONS", - }, - cli.StringFlag{ - Name: "vars", - Usage: "a map of variables to pass to the Terraform `plan` and `apply` commands. Each value is passed as a `=` option", - EnvVar: "PLUGIN_VARS", - }, - cli.StringFlag{ - Name: "secrets", - Usage: "a map of secrets to pass to the Terraform `plan` and `apply` commands. Each value is passed as a `=` option", - EnvVar: "PLUGIN_SECRETS", + cli.StringSliceFlag{ + Name: "actions", + Usage: "a list of actions to have terraform perform", + EnvVar: "PLUGIN_ACTIONS", + Value: &cli.StringSlice{"validate", "plan", "apply"}, }, cli.StringFlag{ Name: "ca_cert", Usage: "ca cert to add to your environment to allow terraform to use internal/private resources", EnvVar: "PLUGIN_CA_CERT", }, - cli.BoolFlag{ - Name: "sensitive", - Usage: "whether or not to suppress terraform commands to stdout", - EnvVar: "PLUGIN_SENSITIVE", + cli.StringFlag{ + Name: "env-file", + Usage: "source env file", + }, + cli.StringFlag{ + Name: "init_options", + Usage: "options for the init command. See https://www.terraform.io/docs/commands/init.html", + EnvVar: "PLUGIN_INIT_OPTIONS", + }, + cli.IntFlag{ + Name: "parallelism", + Usage: "The number of concurrent operations as Terraform walks its graph", + EnvVar: "PLUGIN_PARALLELISM", }, cli.StringFlag{ Name: "netrc.machine", @@ -78,38 +73,36 @@ func main() { Usage: "The root directory where the terraform files live. When unset, the top level directory will be assumed", EnvVar: "PLUGIN_ROOT_DIR", }, - cli.IntFlag{ - Name: "parallelism", - Usage: "The number of concurrent operations as Terraform walks its graph", - EnvVar: "PLUGIN_PARALLELISM", - }, - cli.StringFlag{ - Name: "env-file", - Usage: "source env file", + Name: "secrets", + Usage: "a map of secrets to pass to the Terraform `plan` and `apply` commands. Each value is passed as a `=` option", + EnvVar: "PLUGIN_SECRETS", + }, + cli.BoolFlag{ + Name: "sensitive", + Usage: "whether or not to suppress terraform commands to stdout", + EnvVar: "PLUGIN_SENSITIVE", }, - cli.StringSliceFlag{ Name: "targets", Usage: "targets to run apply or plan on", EnvVar: "PLUGIN_TARGETS", }, - - cli.StringSliceFlag{ - Name: "var_files", - Usage: "a list of var files to use. Each value is passed as -var-file=", - EnvVar: "PLUGIN_VAR_FILES", - }, - cli.BoolFlag{ - Name: "destroy", - Usage: "destory all resurces", - EnvVar: "PLUGIN_DESTROY", - }, cli.StringFlag{ Name: "tf.version", Usage: "terraform version to use", EnvVar: "PLUGIN_TF_VERSION", }, + cli.StringFlag{ + Name: "vars", + Usage: "a map of variables to pass to the Terraform `plan` and `apply` commands. Each value is passed as a `=` option", + EnvVar: "PLUGIN_VARS", + }, + cli.StringSliceFlag{ + Name: "var_files", + Usage: "a list of var files to use. Each value is passed as -var-file=", + EnvVar: "PLUGIN_VAR_FILES", + }, } if err := app.Run(os.Args); err != nil { @@ -144,7 +137,7 @@ func run(c *cli.Context) error { plugin := Plugin{ Config: Config{ - Plan: c.Bool("plan"), + Actions: c.StringSlice("actions"), Vars: vars, Secrets: secrets, InitOptions: initOptions, @@ -155,7 +148,6 @@ func run(c *cli.Context) error { Parallelism: c.Int("parallelism"), Targets: c.StringSlice("targets"), VarFiles: c.StringSlice("var_files"), - Destroy: c.Bool("destroy"), }, Netrc: Netrc{ Login: c.String("netrc.username"), diff --git a/plugin.go b/plugin.go index 899526d..7321c03 100644 --- a/plugin.go +++ b/plugin.go @@ -21,7 +21,7 @@ import ( type ( // Config holds input parameters for the plugin Config struct { - Plan bool + Actions []string Vars map[string]string Secrets map[string]string InitOptions InitOptions @@ -32,7 +32,6 @@ type ( Parallelism int Targets []string VarFiles []string - Destroy bool } Netrc struct { @@ -88,15 +87,27 @@ func (p Plugin) Exec() error { } commands = append(commands, deleteCache()) - commands = append(commands, initCommand(p.Config.InitOptions)) - commands = append(commands, getModules()) - commands = append(commands, validateCommand(p.Config)) - commands = append(commands, planCommand(p.Config)) - if !p.Config.Plan { - commands = append(commands, terraformCommand(p.Config)) + + // Add commands listed from Actions + for _, action := range p.Config.Actions { + switch action { + case "validate": + commands = append(commands, tfValidate(p.Config)) + 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: validate, plan, apply, plan-destroy, destroy. You provided %s", action) + } } + commands = append(commands, deleteCache()) for _, c := range commands { @@ -127,13 +138,7 @@ func (p Plugin) Exec() error { return nil } -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", - ) -} - +// CopyTfEnv creates copies of TF_VAR_ to lowercase func CopyTfEnv() { tfVar := regexp.MustCompile(`^TF_VAR_.*$`) for _, e := range os.Environ() { @@ -145,6 +150,27 @@ func CopyTfEnv() { } } +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() *exec.Cmd { return exec.Command( "rm", @@ -153,6 +179,13 @@ func deleteCache() *exec.Cmd { ) } +func getModules() *exec.Cmd { + return exec.Command( + "terraform", + "get", + ) +} + func initCommand(config InitOptions) *exec.Cmd { args := []string{ "init", @@ -181,35 +214,69 @@ func initCommand(config InitOptions) *exec.Cmd { ) } -func getModules() *exec.Cmd { +func installCaCert(cacert string) *exec.Cmd { + ioutil.WriteFile("/usr/local/share/ca-certificates/ca_cert.crt", []byte(cacert), 0644) return exec.Command( - "terraform", - "get", + "update-ca-certificates", ) } -func validateCommand(config Config) *exec.Cmd { +func trace(cmd *exec.Cmd) { + fmt.Println("$", strings.Join(cmd.Args, " ")) +} + +func tfApply(config Config) *exec.Cmd { args := []string{ - "validate", + "apply", } - for _, v := range config.VarFiles { - args = append(args, "-var-file", fmt.Sprintf("%s", v)) + for _, v := range config.Targets { + args = append(args, "--target", fmt.Sprintf("%s", v)) } - for k, v := range config.Vars { - args = append(args, "-var") - args = append(args, fmt.Sprintf("%s=%s", k, 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, "plan.tfout") return exec.Command( "terraform", args..., ) } -func planCommand(config Config) *exec.Cmd { +func tfDestroy(config Config) *exec.Cmd { + args := []string{ + "destroy", + } + for _, v := range config.Targets { + args = append(args, fmt.Sprintf("-target=%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, "-force") + return exec.Command( + "terraform", + args..., + ) +} + +func tfPlan(config Config, destroy bool) *exec.Cmd { args := []string{ "plan", } - if config.Destroy { + + if destroy { args = append(args, "-destroy") } else { args = append(args, "-out=plan.tfout") @@ -240,85 +307,23 @@ func planCommand(config Config) *exec.Cmd { ) } -func terraformCommand(config Config) *exec.Cmd { - if config.Destroy { - return destroyCommand(config) - } - - return applyCommand(config) -} - -func applyCommand(config Config) *exec.Cmd { +func tfValidate(config Config) *exec.Cmd { args := []string{ - "apply", + "validate", } - for _, v := range config.Targets { - args = append(args, "--target", fmt.Sprintf("%s", v)) + for _, v := range config.VarFiles { + args = append(args, "-var-file", fmt.Sprintf("%s", v)) } - if config.Parallelism > 0 { - args = append(args, fmt.Sprintf("-parallelism=%d", config.Parallelism)) + for k, v := range config.Vars { + args = append(args, "-var") + args = append(args, fmt.Sprintf("%s=%s", k, v)) } - 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, "plan.tfout") return exec.Command( "terraform", args..., ) } -func destroyCommand(config Config) *exec.Cmd { - args := []string{ - "destroy", - } - for _, v := range config.Targets { - args = append(args, fmt.Sprintf("-target=%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, "-force") - return exec.Command( - "terraform", - args..., - ) -} - -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 trace(cmd *exec.Cmd) { - fmt.Println("$", strings.Join(cmd.Args, " ")) -} - // 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 diff --git a/plugin_test.go b/plugin_test.go index 6073d9c..baf59c0 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -3,138 +3,11 @@ package main import ( "os" "os/exec" - "reflect" "testing" . "github.com/franela/goblin" ) -func Test_destroyCommand(t *testing.T) { - type args struct { - config Config - } - tests := []struct { - name string - args args - want *exec.Cmd - }{ - { - "default", - args{config: Config{}}, - exec.Command("terraform", "destroy", "-force"), - }, - { - "with targets", - args{config: Config{Targets: []string{"target1", "target2"}}}, - exec.Command("terraform", "destroy", "-target=target1", "-target=target2", "-force"), - }, - { - "with parallelism", - args{config: Config{Parallelism: 5}}, - exec.Command("terraform", "destroy", "-parallelism=5", "-force"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := destroyCommand(tt.args.config); !reflect.DeepEqual(got, tt.want) { - t.Errorf("destroyCommand() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_applyCommand(t *testing.T) { - type args struct { - config Config - } - tests := []struct { - name string - args args - want *exec.Cmd - }{ - { - "default", - args{config: Config{}}, - exec.Command("terraform", "apply", "plan.tfout"), - }, - { - "with targets", - args{config: Config{Targets: []string{"target1", "target2"}}}, - exec.Command("terraform", "apply", "--target", "target1", "--target", "target2", "plan.tfout"), - }, - { - "with parallelism", - args{config: Config{Parallelism: 5}}, - exec.Command("terraform", "apply", "-parallelism=5", "plan.tfout"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := applyCommand(tt.args.config); !reflect.DeepEqual(got, tt.want) { - t.Errorf("applyCommand() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_terraformCommand(t *testing.T) { - type args struct { - config Config - } - tests := []struct { - name string - args args - want *exec.Cmd - }{ - { - "default", - args{config: Config{}}, - exec.Command("terraform", "apply", "plan.tfout"), - }, - { - "destroy", - args{config: Config{Destroy: true}}, - exec.Command("terraform", "destroy", "-force"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := terraformCommand(tt.args.config); !reflect.DeepEqual(got, tt.want) { - t.Errorf("terraformCommand() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_planCommand(t *testing.T) { - type args struct { - config Config - } - tests := []struct { - name string - args args - want *exec.Cmd - }{ - { - "default", - args{config: Config{}}, - exec.Command("terraform", "plan", "-out=plan.tfout"), - }, - { - "destroy", - args{config: Config{Destroy: true}}, - exec.Command("terraform", "plan", "-destroy"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := planCommand(tt.args.config); !reflect.DeepEqual(got, tt.want) { - t.Errorf("planCommand() = %v, want %v", got, tt.want) - } - }) - } -} - func TestPlugin(t *testing.T) { g := Goblin(t) @@ -153,4 +26,104 @@ func TestPlugin(t *testing.T) { 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 + } + + tests := []struct { + name string + args args + want *exec.Cmd + }{ + { + "default", + args{config: Config{}}, + exec.Command("terraform", "apply", "plan.tfout"), + }, + { + "with targets", + args{config: Config{Targets: []string{"target1", "target2"}}}, + exec.Command("terraform", "apply", "--target", "target1", "--target", "target2", "plan.tfout"), + }, + { + "with parallelism", + args{config: Config{Parallelism: 5}}, + exec.Command("terraform", "apply", "-parallelism=5", "plan.tfout"), + }, + } + + for _, tt := range tests { + g.Assert(tfApply(tt.args.config)).Equal(tt.want) + } + }) + }) + + g.Describe("tfDestroy", func() { + g.It("Should return correct destroy commands given the arguments", func() { + type args struct { + config Config + } + + tests := []struct { + name string + args args + want *exec.Cmd + }{ + { + "default", + args{config: Config{}}, + exec.Command("terraform", "destroy", "-force"), + }, + { + "with targets", + args{config: Config{Targets: []string{"target1", "target2"}}}, + exec.Command("terraform", "destroy", "-target=target1", "-target=target2", "-force"), + }, + { + "with parallelism", + args{config: Config{Parallelism: 5}}, + exec.Command("terraform", "destroy", "-parallelism=5", "-force"), + }, + } + + for _, tt := range tests { + g.Assert(tfDestroy(tt.args.config)).Equal(tt.want) + } + }) + }) + + g.Describe("tfPlan", func() { + g.It("Should return correct plan commands given the arguments", func() { + type args struct { + config Config + } + + 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"), + }, + } + + for _, tt := range tests { + g.Assert(tfPlan(tt.args.config, tt.destroy)).Equal(tt.want) + } + }) + }) }