Skip to content

Commit

Permalink
Move integration test and add backend check (#5)
Browse files Browse the repository at this point in the history
The integration test no longer builds and uses the binary
but runs the Cobra command directly. Also, an additional
check is added that makes sure the backend Terraform was
initialized with matches the backend configuration of the
current run. This avoids that state is accidentally pushed
to the wrong backend when switching environments.
  • Loading branch information
unguiculus authored Mar 5, 2020
1 parent accfc0a commit 9a551a2
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 104 deletions.
25 changes: 15 additions & 10 deletions cmd/gotf/gotf.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ import (
)

func Execute() {
command := newGotfCommand()
if err := command.Execute(); err != nil {
var exitCode int
if err, ok := err.(*exec.ExitError); ok {
exitCode = err.ExitCode()
} else {
exitCode = 1
}
os.Exit(exitCode)
}
}

func newGotfCommand() *cobra.Command {
var cfgFile string
params := opts.NewMapOpts()
var debug bool
Expand Down Expand Up @@ -69,17 +82,9 @@ gotf is a Terraform wrapper facilitating configurations for various environments
command.Flags().StringVarP(&moduleDir, "module-dir", "m", "", "The module directory to run Terraform in")
command.Flags().SetInterspersed(false)
command.SetVersionTemplate("{{ .Version }}\n")
command.SilenceUsage = true
if err := command.MarkFlagRequired("module-dir"); err != nil {
log.Fatalln(err)
}

if err := command.Execute(); err != nil {
var exitCode int
if err, ok := err.(*exec.ExitError); ok {
exitCode = err.ExitCode()
} else {
exitCode = 1
}
os.Exit(exitCode)
}
return command
}
143 changes: 143 additions & 0 deletions cmd/gotf/gotf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package gotf

import (
"io/ioutil"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func TestExecute(t *testing.T) {
type testRun struct {
args []string
want []string
wantErr bool
}

tests := []struct {
name string
runs []testRun
}{
{
name: "happy path",
runs: []testRun{
{
args: []string{"-d", "-c", "testdata/test-config-prod.yaml", "-p", "env=prod", "-m", "testdata/01_testmodule", "init", "-no-color"},
want: []string{"Terraform has been successfully initialized!"},
},
{
args: []string{"-d", "-c", "testdata/test-config-prod.yaml", "-p", "env=prod", "-m", "testdata/01_testmodule", "plan", "-no-color"},
want: []string{
"# null_resource.echo will be created",
"Plan: 1 to add, 0 to change, 0 to destroy.",
},
},
{
args: []string{"-d", "-c", "testdata/test-config-prod.yaml", "-p", "env=prod", "-m", "testdata/01_testmodule", "apply", "-auto-approve", "-no-color"},
want: []string{`foo = 42
mapvar = {
entry1 = {
value1 = testvalue1
value2 = true
}
entry2 = {
value1 = testvalue2
value2 = false
}
}`},
},
},
},
{
name: "backend check",
runs: []testRun{
{
args: []string{"-d", "-c", "testdata/test-config-prod.yaml", "-p", "env=prod", "-m", "testdata/01_testmodule", "init", "-no-color"},
want: []string{"Terraform has been successfully initialized!"},
},
{
args: []string{"-d", "-c", "testdata/test-config-prod.yaml", "-p", "env=prod", "-m", "testdata/01_testmodule", "plan", "-no-color"},
want: []string{
"# null_resource.echo will be created",
"Plan: 1 to add, 0 to change, 0 to destroy.",
},
},
{
args: []string{"-d", "-c", "testdata/test-config-dev.yaml", "-p", "env=dev", "-m", "testdata/01_testmodule", "plan", "-no-color"},
want: []string{
"Configured backend does not match current environment",
"Got: .terraform/terraform-testmodule-prod.tfstate",
"Want: .terraform/terraform-testmodule-dev.tfstate",
"Run terraform init -reconfigure!",
},
wantErr: true,
},
{
args: []string{"-d", "-c", "testdata/test-config-dev.yaml", "-p", "env=dev", "-m", "testdata/01_testmodule", "init", "-reconfigure"},
want: []string{
"Terraform has been successfully initialized!",
},
},
{
args: []string{"-d", "-c", "testdata/test-config-dev.yaml", "-p", "env=dev", "-m", "testdata/01_testmodule", "plan", "-no-color"},
want: []string{
"# null_resource.echo will be created",
"Plan: 1 to add, 0 to change, 0 to destroy.",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.RemoveAll("testdata/01_testmodule/.terraform")

tempDir, err := ioutil.TempDir("", "gotf")
panicOnError(err)
defer os.RemoveAll(tempDir)

panicOnError(os.Setenv("XDG_CACHE_HOME", filepath.Join(tempDir, "test")))

for _, run := range tt.runs {
got, err := runGotf(run.args)
if run.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
for _, want := range run.want {
assert.Contains(t, got, want)
}
}
})
}
}

func panicOnError(err error) {
if err != nil {
panic(err)
}
}

func runGotf(args []string) (string, error) {
oldStdout := os.Stdout
oldStderr := os.Stderr
defer func() {
os.Stdout = oldStdout
os.Stderr = oldStderr
}()

command := newGotfCommand()
r, w, _ := os.Pipe()
os.Stdout = w
os.Stderr = w
command.SetArgs(args)
err := command.Execute()
w.Close()
bytes, _ := ioutil.ReadAll(r)
output := string(bytes)
println(output)
return output, err
}
1 change: 1 addition & 0 deletions cmd/gotf/testdata/01_testmodule/test-prod.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bar = "bazvalue"
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ vars:
envs:
BAR: barvalue
backendConfigs:
path: .tfstate/terraform.tfstate
path: .terraform/terraform-{{ .Vars.state_key }}-dev.tfstate
22 changes: 22 additions & 0 deletions cmd/gotf/testdata/test-config-prod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
terraformVersion: "0.12.20"
varFiles:
- 01_testmodule/test-{{ .Params.env }}.tfvars
vars:
foo: 42
mapvar: |-
{
entry1 = {
value1 = testvalue1
value2 = true
}
entry2 = {
value1 = testvalue2
value2 = false
}
}
module_dir: "{{ base .Params.moduleDir }}"
state_key: '{{ (splitn "_" 2 (base .Params.moduleDir))._1 }}'
envs:
BAR: barvalue
backendConfigs:
path: .terraform/terraform-{{ .Vars.state_key }}-prod.tfstate
47 changes: 47 additions & 0 deletions pkg/tf/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
package terraform

import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/craftypath/gotf/pkg/config"
Expand Down Expand Up @@ -54,6 +58,9 @@ func (tf *Terraform) Execute(args ...string) error {
tf.appendVarArgs(env)
tf.appendBackendConfigs(env)

if err := tf.checkBackendConfig(args...); err != nil {
return err
}
return tf.shell.Execute(env, tf.moduleDir, tf.binaryPath, args...)
}

Expand Down Expand Up @@ -95,6 +102,46 @@ func (tf *Terraform) appendBackendConfigs(env map[string]string) {
}
}

func (tf *Terraform) checkBackendConfig(args ...string) error {
if len(args) >= 2 {
if args[0] == "init" {
for _, arg := range args[1:] {
if arg == "-reconfigure" {
return nil
}
}
}
}

backendFile := filepath.Join(tf.moduleDir, ".terraform", "terraform.tfstate")
bytes, err := ioutil.ReadFile(backendFile)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}

var backendJson map[string]interface{}
if err := json.Unmarshal(bytes, &backendJson); err != nil {
return err
}

for k, v := range tf.config.BackendConfigs {
b := backendJson["backend"].(map[string]interface{})
c := b["config"].(map[string]interface{})
currentVal := c[k]
if v != currentVal {
return fmt.Errorf(`Configured backend does not match current environment
Got: %s
Want: %s
Run terraform init -reconfigure!`, currentVal, v)
}
}
return nil
}

func stringMapAppend(target map[string]string, src map[string]string) {
for k, v := range src {
target[k] = v
Expand Down
93 changes: 0 additions & 93 deletions test/integration_test.go

This file was deleted.

0 comments on commit 9a551a2

Please sign in to comment.