diff --git a/DOCS.md b/DOCS.md index 1c58a15..e595201 100644 --- a/DOCS.md +++ b/DOCS.md @@ -6,6 +6,8 @@ Use the Terraform plugin to apply the infrastructure configuration contained wit * `config` - a map of configuration parameters for the remote state backend. Each value is passed as a `-backend-config==` option. * `vars` - a map of variables to pass to the Terraform `plan` and `apply` commands. Each value is passed as a `-var =` option. +* `secrets` - a map of variables to pass to the Terraform `plan` and `apply` commands. Each value is passed as a `-var + =` option. The `ENVVAR` is read as the key/pair value. * `ca_cert` - ca cert to add to your environment to allow terraform to use internal/private resources * `sensitive` (default: `false`) - Whether or not to suppress terraform commands to stdout. * `role_arn_to_assume` - A role to assume before running the terraform commands. @@ -27,6 +29,8 @@ deploy: vars: app_name: my-project app_version: 1.0.0 + secrets: + my_secret: TERRAFORM_SECRET ``` # Advanced Configuration diff --git a/Dockerfile b/Dockerfile index ed99208..dd868e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ FROM gliderlabs/alpine:3.2 RUN apk-install ca-certificates git -ENV TERRAFORM_VERSION 0.6.16 +ENV TERRAFORM_VERSION 0.7.5 RUN apk update && \ wget -q "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.21-r2/glibc-2.21-r2.apk" && \ diff --git a/main.go b/main.go index 951494b..2e36ae0 100644 --- a/main.go +++ b/main.go @@ -1,188 +1,115 @@ package main import ( - "fmt" - "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/drone/drone-plugin-go/plugin" - "io/ioutil" + "encoding/json" "os" - "os/exec" - "strings" - "time" + + "github.com/joho/godotenv" + "github.com/Sirupsen/logrus" + "github.com/urfave/cli" ) -var ( - buildCommit string -) - -type terraform struct { - Remote remote `json:"remote"` - Plan bool `json:"plan"` - Vars map[string]string `json:"vars"` - Cacert string `json:"ca_cert"` - Sensitive bool `json:"sensitive"` - RoleARN string `json:"role_arn_to_assume"` - RootDir string `json:"root_dir"` - Parallelism int `json:"parallelism"` -} - -type remote struct { - Backend string `json:"backend"` - Config map[string]string `json:"config"` -} +var version string // build number set at compile-time func main() { - fmt.Printf("Drone Terraform Plugin built from %s\n", buildCommit) + app := cli.NewApp() + app.Name = "terraform plugin" + app.Usage = "terraform plugin" + app.Action = run + app.Version = version + app.Flags = []cli.Flag{ - workspace := plugin.Workspace{} - vargs := terraform{} + // + // plugin args + // - plugin.Param("workspace", &workspace) - plugin.Param("vargs", &vargs) - plugin.MustParse() + cli.BoolFlag{ + Name: "terraform.plan", + Usage: "calculates a plan but does NOT apply it", + EnvVar: "PLUGIN_PLAN", + }, + cli.StringFlag{ + Name: "terraform.remote", + Usage: "contains the configuration for the Terraform remote state tracking", + EnvVar: "PLUGIN_REMOTE", + }, + cli.StringFlag{ + Name: "terraform.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: "terraform.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.StringFlag{ + Name: "terraform.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: "terraform.sensitive", + Usage: "whether or not to suppress terraform commands to stdout", + EnvVar: "PLUGIN_SENSITIVE", + }, + cli.StringFlag{ + Name: "terraform.role_arn_to_assume", + Usage: "A role to assume before running the terraform commands", + EnvVar: "PLUGIN_ROLE_ARN_TO_ASSUME", + }, + cli.StringFlag{ + Name: "terraform.root_dir", + 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: "terraform.parallelism", + Usage: "The number of concurrent operations as Terraform walks its graph", + EnvVar: "PLUGIN_PARALLELISM", + }, - if vargs.RoleARN != "" { - assumeRole(vargs.RoleARN) + cli.StringFlag{ + Name: "env-file", + Usage: "source env file", + }, } - var commands []*exec.Cmd - remote := vargs.Remote - if vargs.Cacert != "" { - commands = append(commands, installCaCert(vargs.Cacert)) + if err := app.Run(os.Args); err != nil { + logrus.Fatal(err) } - if remote.Backend != "" { - commands = append(commands, deleteCache()) - commands = append(commands, remoteConfigCommand(remote)) - } - commands = append(commands, getModules()) - commands = append(commands, planCommand(vargs.Vars, vargs.Parallelism)) - if !vargs.Plan { - commands = append(commands, applyCommand(vargs.Parallelism)) - } - commands = append(commands, deleteCache()) - - for _, c := range commands { - c.Env = os.Environ() - c.Dir = workspace.Path - if c.Dir == "" { - wd, err := os.Getwd() - if err == nil { - c.Dir = wd - } - } - if vargs.RootDir != "" { - c.Dir = c.Dir + "/" + vargs.RootDir - } - c.Stdout = os.Stdout - c.Stderr = os.Stderr - if !vargs.Sensitive { - trace(c) - } - - err := c.Run() - if err != nil { - fmt.Println("Error!") - fmt.Println(err) - os.Exit(1) - } - fmt.Println("Command completed successfully") - } - } -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", - ) -} - -func deleteCache() *exec.Cmd { - return exec.Command( - "rm", - "-rf", - ".terraform", - ) -} - -func remoteConfigCommand(config remote) *exec.Cmd { - args := []string{ - "remote", - "config", - fmt.Sprintf("-backend=%s", config.Backend), - } - for k, v := range config.Config { - args = append(args, fmt.Sprintf("-backend-config=%s=%s", k, v)) - } - return exec.Command( - "terraform", - args..., - ) -} - -func getModules() *exec.Cmd { - return exec.Command( - "terraform", - "get", - ) -} - -func planCommand(variables map[string]string, parallelism int) *exec.Cmd { - args := []string{ - "plan", - "-out=plan.tfout", - } - for k, v := range variables { - args = append(args, "-var") - args = append(args, fmt.Sprintf("%s=%s", k, v)) - } - if parallelism > 0 { - args = append(args, fmt.Sprintf("-parallelism=%d", parallelism)) - } - return exec.Command( - "terraform", - args..., - ) -} - -func applyCommand(parallelism int) *exec.Cmd { - args := []string{ - "apply", - } - if parallelism > 0 { - args = append(args, fmt.Sprintf("-parallelism=%d", parallelism)) - } - args = append(args, "plan.tfout") - 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", +func run(c *cli.Context) error { + if c.String("env-file") != "" { + _ = godotenv.Load(c.String("env-file")) } - value, err := credentials.NewCredentials(stsProvider).Get() - if err != nil { - fmt.Println("Error assuming role!") - fmt.Println(err) - os.Exit(1) - } - os.Setenv("AWS_ACCESS_KEY_ID", value.AccessKeyID) - os.Setenv("AWS_SECRET_ACCESS_KEY", value.SecretAccessKey) - os.Setenv("AWS_SESSION_TOKEN", value.SessionToken) -} + remote := Remote{} + json.Unmarshal([]byte(c.String("terraform.remote")), &remote) -func trace(cmd *exec.Cmd) { - fmt.Println("$", strings.Join(cmd.Args, " ")) + var vars map[string]string + if err := json.Unmarshal([]byte(c.String("terraform.vars")), &vars); err != nil { + panic(err) + } + var secrets map[string]string + if err := json.Unmarshal([]byte(c.String("terraform.secrets")), &secrets); err != nil { + panic(err) + } + + plugin := Plugin{ + Config: Config{ + Remote: remote, + Plan: c.Bool("terraform.plan"), + Vars: vars, + Secrets: secrets, + Cacert: c.String("terraform.ca_cert"), + Sensitive: c.Bool("terraform.sensitive"), + RoleARN: c.String("terraform.role_arn_to_assume"), + RootDir: c.String("terraform.root_dir"), + Parallelism: c.Int("terraform.parallelism"), + }, + } + + return plugin.Exec() } diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..34c6031 --- /dev/null +++ b/plugin.go @@ -0,0 +1,190 @@ +package main + +import ( + "fmt" + "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" + "io/ioutil" + "os" + "os/exec" + "strings" + "time" +) + +var ( + buildCommit string +) + +type ( + Config struct { + Remote Remote + Plan bool + Vars map[string]string + Secrets map[string]string + Cacert string + Sensitive bool + RoleARN string + RootDir string + Parallelism int + } + + Remote struct { + Backend string `json:"backend"` + Config map[string]string `json:"config"` + } + + Plugin struct { + Config Config + } +) + +func (p Plugin) Exec() error { + fmt.Printf("Drone Terraform Plugin built from %s\n", buildCommit) + + if p.Config.RoleARN != "" { + assumeRole(p.Config.RoleARN) + } + + var commands []*exec.Cmd + remote := p.Config.Remote + if p.Config.Cacert != "" { + commands = append(commands, installCaCert(p.Config.Cacert)) + } + if remote.Backend != "" { + commands = append(commands, deleteCache()) + commands = append(commands, remoteConfigCommand(remote)) + } + commands = append(commands, getModules()) + commands = append(commands, planCommand(p.Config.Vars, p.Config.Secrets, p.Config.Parallelism)) + if !p.Config.Plan { + commands = append(commands, applyCommand(p.Config.Parallelism)) + } + commands = append(commands, deleteCache()) + + for _, c := range commands { + if c.Dir == "" { + wd, err := os.Getwd() + if err == nil { + c.Dir = wd + } + } + 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 { + fmt.Println("Error!") + fmt.Println(err) + os.Exit(1) + } + fmt.Println("Command completed successfully") + } + + 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", + ) +} + +func deleteCache() *exec.Cmd { + return exec.Command( + "rm", + "-rf", + ".terraform", + ) +} + +func remoteConfigCommand(config Remote) *exec.Cmd { + args := []string{ + "remote", + "config", + fmt.Sprintf("-backend=%s", config.Backend), + } + for k, v := range config.Config { + args = append(args, fmt.Sprintf("-backend-config=%s=%s", k, v)) + } + return exec.Command( + "terraform", + args..., + ) +} + +func getModules() *exec.Cmd { + return exec.Command( + "terraform", + "get", + ) +} + +func planCommand(variables map[string]string, secrets map[string]string, parallelism int) *exec.Cmd { + args := []string{ + "plan", + "-out=plan.tfout", + } + for k, v := range variables { + args = append(args, "-var") + args = append(args, fmt.Sprintf("%s=%s", k, v)) + } + for k, v := range secrets { + args = append(args, "-var") + args = append(args, fmt.Sprintf("%s=%s", k, os.Getenv(v))) + } + if parallelism > 0 { + args = append(args, fmt.Sprintf("-parallelism=%d", parallelism)) + } + return exec.Command( + "terraform", + args..., + ) +} + +func applyCommand(parallelism int) *exec.Cmd { + args := []string{ + "apply", + } + if parallelism > 0 { + args = append(args, fmt.Sprintf("-parallelism=%d", parallelism)) + } + args = append(args, "plan.tfout") + 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 { + fmt.Println("Error assuming role!") + fmt.Println(err) + os.Exit(1) + } + 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, " ")) +}