Browse Source

Merge 9cabe5144b into 082b4073ad

pull/63/merge
Jonathan Wright 7 years ago
committed by GitHub
parent
commit
17b79f60ee
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      .drone.yml
  2. 2
      Dockerfile
  3. 25
      README.md
  4. 92
      main.go
  5. 215
      plugin.go
  6. 227
      plugin_test.go

12
.drone.yml

@ -1,12 +1,20 @@
workspace:
base: /go
path: src/github.com/drone-plugins/drone-terraform
pipeline:
test:
image: golang:1.8
image: golang:1.9
environment:
- CGO_ENABLED=0
commands:
- go test -cover -coverprofile=coverage.out
- go build -ldflags "-s -w -X main.revision=$(git rev-parse HEAD)" -a
build-container:
image: plugins/docker
secrets: [ docker_username, docker_password ]
repo: jonathanio/drone-terraform
auto_tag: true
when:
branch: master

2
Dockerfile

@ -16,7 +16,7 @@ RUN apk -U add \
wget && \
rm -rf /var/cache/apk/*
ENV TERRAFORM_VERSION 0.11.3
ENV TERRAFORM_VERSION 0.11.7
RUN wget -q https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip -O terraform.zip && \
unzip terraform.zip -d /bin && \
rm -f terraform.zip

25
README.md

@ -1,15 +1,16 @@
# drone-terraform
[![Build Status](http://beta.drone.io/api/badges/jmccann/drone-terraform/status.svg)](http://beta.drone.io/jmccann/drone-terraform)
[![Build Status](https://drone.nwk.io/api/badges/jonathanio/drone-terraform/status.svg)](https://drone.nwk.io/jonathanio/drone-terraform)
Drone plugin to execute Terraform plan and apply. For the usage information and
a listing of the available options please take a look at [the docs](https://github.com/jmccann/drone-terraform/blob/master/DOCS.md).
Drone plugin to execute Terraform. For the usage information and a listing of
the available options please take a look at [the
docs](https://github.com/jonathanio/drone-terraform/blob/master/DOCS.md).
## Build
Build the binary with the following commands:
```
```bash
go build
go test
```
@ -18,15 +19,15 @@ go test
Build the docker image with the following commands:
```
```bash
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags netgo
docker build --rm=true -t jmccann/drone-terraform .
docker build --rm=true -t jonathanio/drone-terraform .
```
Please note incorrectly building the image for the correct x64 linux and with
GCO disabled will result in an error when running the Docker image:
```
```text
docker: Error response from daemon: Container command
'/bin/drone-terraform' not found or does not exist.
```
@ -35,13 +36,15 @@ docker: Error response from daemon: Container command
Execute from the working directory:
```
```bash
docker run --rm \
-v $(pwd):$(pwd) \
-w $(pwd) \
jmccann/drone-terraform:latest --plan
jonathanio/drone-terraform:latest --plan
```
## Drone 0.4
## Legacy and Upstream
Legacy `drone-terraform` plugin exists @ `jmccann/drone-terraform:0.4`
Access to older versions and upstream versions of `drone-terraform` is
available from the master repository at
[jmccann/drone-terraform](https://github.com/jmccann/drone-terraform).

92
main.go

@ -23,35 +23,45 @@ func main() {
// plugin args
//
cli.BoolFlag{
Name: "plan",
Usage: "calculates a plan but does NOT apply it",
EnvVar: "PLUGIN_PLAN",
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.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.StringFlag{
Name: "vars",
Usage: "a map of variables to pass to the Terraform `plan` and `apply` commands. Each value is passed as a `<key>=<value>` option",
EnvVar: "PLUGIN_VARS",
cli.IntFlag{
Name: "parallelism",
Usage: "The number of concurrent operations as Terraform walks its graph",
EnvVar: "PLUGIN_PARALLELISM",
},
cli.StringFlag{
Name: "secrets",
Usage: "a map of secrets to pass to the Terraform `plan` and `apply` commands. Each value is passed as a `<key>=<ENV>` option",
EnvVar: "PLUGIN_SECRETS",
Name: "netrc.machine",
Usage: "netrc machine",
EnvVar: "DRONE_NETRC_MACHINE",
},
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",
Name: "netrc.username",
Usage: "netrc username",
EnvVar: "DRONE_NETRC_USERNAME",
},
cli.BoolFlag{
Name: "sensitive",
Usage: "whether or not to suppress terraform commands to stdout",
EnvVar: "PLUGIN_SENSITIVE",
cli.StringFlag{
Name: "netrc.password",
Usage: "netrc password",
EnvVar: "DRONE_NETRC_PASSWORD",
},
cli.StringFlag{
Name: "role_arn_to_assume",
@ -63,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 `<key>=<ENV>` 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=<value>",
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 `<key>=<value>` option",
EnvVar: "PLUGIN_VARS",
},
cli.StringSliceFlag{
Name: "var_files",
Usage: "a list of var files to use. Each value is passed as -var-file=<value>",
EnvVar: "PLUGIN_VAR_FILES",
},
}
if err := app.Run(os.Args); err != nil {
@ -129,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,
@ -140,7 +148,11 @@ 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"),
Machine: c.String("netrc.machine"),
Password: c.String("netrc.password"),
},
Terraform: Terraform{
Version: c.String("tf.version"),

215
plugin.go

@ -5,6 +5,8 @@ import (
"io/ioutil"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"strings"
"time"
@ -19,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
@ -30,7 +32,12 @@ type (
Parallelism int
Targets []string
VarFiles []string
Destroy bool
}
Netrc struct {
Machine string
Login string
Password string
}
// InitOptions include options for the Terraform's init command
@ -43,6 +50,7 @@ type (
// Plugin represents the plugin instance to be executed
Plugin struct {
Config Config
Netrc Netrc
Terraform Terraform
}
)
@ -62,6 +70,12 @@ func (p Plugin) Exec() error {
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 commands []*exec.Cmd
commands = append(commands, exec.Command("terraform", "version"))
@ -73,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 {
@ -112,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() {
@ -130,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",
@ -138,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",
@ -166,50 +214,24 @@ 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 {
args := []string{
"validate",
}
for _, v := range config.VarFiles {
args = append(args, "-var-file", fmt.Sprintf("%s", v))
}
for k, v := range config.Vars {
args = append(args, "-var")
args = append(args, fmt.Sprintf("%s=%s", k, v))
}
return exec.Command(
"terraform",
args...,
)
func trace(cmd *exec.Cmd) {
fmt.Println("$", strings.Join(cmd.Args, " "))
}
func planCommand(config Config) *exec.Cmd {
func tfApply(config Config) *exec.Cmd {
args := []string{
"plan",
}
if config.Destroy {
args = append(args, "-destroy")
} else {
args = append(args, "-out=plan.tfout")
"apply",
}
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))
}
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))
}
@ -219,26 +241,19 @@ func planCommand(config Config) *exec.Cmd {
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 terraformCommand(config Config) *exec.Cmd {
if config.Destroy {
return destroyCommand(config)
}
return applyCommand(config)
}
func applyCommand(config Config) *exec.Cmd {
func tfDestroy(config Config) *exec.Cmd {
args := []string{
"apply",
"destroy",
}
for _, v := range config.Targets {
args = append(args, "--target", fmt.Sprintf("%s", v))
args = append(args, fmt.Sprintf("-target=%s", v))
}
if config.Parallelism > 0 {
args = append(args, fmt.Sprintf("-parallelism=%d", config.Parallelism))
@ -249,19 +264,33 @@ func applyCommand(config Config) *exec.Cmd {
if config.InitOptions.LockTimeout != "" {
args = append(args, fmt.Sprintf("-lock-timeout=%s", config.InitOptions.LockTimeout))
}
args = append(args, "plan.tfout")
args = append(args, "-force")
return exec.Command(
"terraform",
args...,
)
}
func destroyCommand(config Config) *exec.Cmd {
func tfPlan(config Config, destroy bool) *exec.Cmd {
args := []string{
"destroy",
"plan",
}
if destroy {
args = append(args, "-destroy")
} else {
args = append(args, "-out=plan.tfout")
}
for _, v := range config.Targets {
args = append(args, fmt.Sprintf("-target=%s", v))
args = append(args, "--target", fmt.Sprintf("%s", v))
}
for _, v := range config.VarFiles {
args = append(args, "-var-file", 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))
@ -272,34 +301,54 @@ func destroyCommand(config Config) *exec.Cmd {
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",
func tfValidate(config Config) *exec.Cmd {
args := []string{
"validate",
}
value, err := credentials.NewCredentials(stsProvider).Get()
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Error assuming role!")
for _, v := range config.VarFiles {
args = append(args, "-var-file", fmt.Sprintf("%s", v))
}
os.Setenv("AWS_ACCESS_KEY_ID", value.AccessKeyID)
os.Setenv("AWS_SECRET_ACCESS_KEY", value.SecretAccessKey)
os.Setenv("AWS_SESSION_TOKEN", value.SessionToken)
for k, v := range config.Vars {
args = append(args, "-var")
args = append(args, fmt.Sprintf("%s=%s", k, v))
}
return exec.Command(
"terraform",
args...,
)
}
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
func writeNetrc(machine, login, password string) error {
if machine == "" {
return nil
}
out := fmt.Sprintf(
netrcFile,
machine,
login,
password,
)
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
`

227
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)
}
})
})
}

Loading…
Cancel
Save