From 3bc64f98b59f5313aade1f0998cbd68f9e4c2021 Mon Sep 17 00:00:00 2001 From: Paul Tyng Date: Sat, 18 Jul 2020 21:51:57 -0400 Subject: [PATCH] Fix -var handling Add apply test with -var usage, simplify exec.Cmd assertions --- tfexec/apply.go | 2 +- tfexec/apply_test.go | 129 ++++++++++++++---- tfexec/cmd.go | 103 ++++++++++++++ tfexec/cmd_test.go | 51 +++++++ tfexec/destroy.go | 2 +- tfexec/destroy_test.go | 60 +++++--- tfexec/import.go | 2 +- tfexec/import_test.go | 76 ++++++----- tfexec/init_test.go | 60 +++++--- tfexec/output_test.go | 47 +++---- tfexec/plan.go | 2 +- tfexec/plan_test.go | 58 +++++--- tfexec/providers_schema_test.go | 18 +-- tfexec/show_test.go | 20 +-- tfexec/terraform_cmd.go | 93 ------------- tfexec/terraform_test.go | 82 +++++++---- tfexec/testdata/state/main.tf | 3 + .../{basic => state}/terraform.tfstate | 0 tfexec/testdata/var/main.tf | 8 ++ 19 files changed, 529 insertions(+), 287 deletions(-) create mode 100644 tfexec/cmd.go create mode 100644 tfexec/cmd_test.go delete mode 100644 tfexec/terraform_cmd.go create mode 100644 tfexec/testdata/state/main.tf rename tfexec/testdata/{basic => state}/terraform.tfstate (100%) create mode 100644 tfexec/testdata/var/main.tf diff --git a/tfexec/apply.go b/tfexec/apply.go index 48bc4ce0..fe3fb758 100644 --- a/tfexec/apply.go +++ b/tfexec/apply.go @@ -133,7 +133,7 @@ func (tf *Terraform) applyCmd(ctx context.Context, opts ...ApplyOption) *exec.Cm } if c.vars != nil { for _, v := range c.vars { - args = append(args, "-var '"+v+"'") + args = append(args, "-var", v) } } diff --git a/tfexec/apply_test.go b/tfexec/apply_test.go index 12917ce2..7f1a306e 100644 --- a/tfexec/apply_test.go +++ b/tfexec/apply_test.go @@ -2,34 +2,83 @@ package tfexec import ( "context" + "encoding/json" + "fmt" "os" "path/filepath" - "strings" "testing" ) func TestApply(t *testing.T) { - td := testTempDir(t) - defer os.RemoveAll(td) + ctx := context.Background() - tf, err := NewTerraform(td, tfVersion(t, "0.12.28")) - if err != nil { - t.Fatal(err) - } + for _, c := range []struct { + version string + configDir string + checkOutput bool + }{ + {"0.11.14", "basic", false}, + {"0.12.28", "basic", false}, + {"0.13.0-beta3", "basic", false}, - err = copyFile(filepath.Join(testFixtureDir, "basic/main.tf"), td) - if err != nil { - t.Fatalf("error copying config file into test dir: %s", err) - } + {"0.12.28", "var", true}, + {"0.13.0-beta3", "var", true}, + } { + testName := fmt.Sprintf(fmt.Sprintf("%s %s", c.version, c.configDir)) + t.Run(testName, func(t *testing.T) { + td := testTempDir(t) + defer os.RemoveAll(td) - err = tf.Init(context.Background()) - if err != nil { - t.Fatalf("error running Init in test directory: %s", err) - } + err := copyFiles(filepath.Join(testFixtureDir, c.configDir), td) + if err != nil { + t.Fatal(err) + } - err = tf.Apply(context.Background()) - if err != nil { - t.Fatalf("error running Apply: %s", err) + tf, err := NewTerraform(td, tfVersion(t, c.version)) + if err != nil { + t.Fatal(err) + } + + err = tf.Init(ctx, Lock(false)) + if err != nil { + t.Fatal(err) + } + + opts := []ApplyOption{} + if c.checkOutput { + opts = append(opts, Var("in="+testName)) + } + + err = tf.Apply(ctx, opts...) + if err != nil { + t.Fatalf("error running Apply: %s", err) + } + + outputs, err := tf.Output(ctx) + if err != nil { + t.Fatal(err) + } + + if !c.checkOutput { + return + } + + if out, ok := outputs["out"]; ok { + var vs string + err = json.Unmarshal(out.Value, &vs) + if err != nil { + t.Fatal(err) + } + + if vs != testName { + t.Fatalf("expected %q, got %q", testName, vs) + } + + return + } + + t.Fatalf("output %q not found", "out") + }) } } @@ -42,13 +91,43 @@ func TestApplyCmd(t *testing.T) { t.Fatal(err) } - applyCmd := tf.applyCmd(context.Background(), Backup("testbackup"), LockTimeout("200s"), State("teststate"), StateOut("teststateout"), VarFile("testvarfile"), Lock(false), Parallelism(99), Refresh(false), Target("target1"), Target("target2"), Var("var1=foo"), Var("var2=bar"), DirOrPlan("testfile")) + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) - actual := strings.TrimPrefix(cmdString(applyCmd), applyCmd.Path+" ") + t.Run("basic", func(t *testing.T) { + applyCmd := tf.applyCmd(context.Background(), + Backup("testbackup"), + LockTimeout("200s"), + State("teststate"), + StateOut("teststateout"), + VarFile("testvarfile"), + Lock(false), + Parallelism(99), + Refresh(false), + Target("target1"), + Target("target2"), + Var("var1=foo"), + Var("var2=bar"), + DirOrPlan("testfile"), + ) - expected := "apply -no-color -auto-approve -backup=testbackup -lock-timeout=200s -state=teststate -state-out=teststateout -var-file=testvarfile -lock=false -parallelism=99 -refresh=false -target=target1 -target=target2 -var 'var1=foo' -var 'var2=bar' testfile" - - if actual != expected { - t.Fatalf("expected arguments of ApplyCmd:\n%s\n actual arguments:\n%s\n", expected, actual) - } + assertCmd(t, []string{ + "apply", + "-no-color", + "-auto-approve", + "-backup=testbackup", + "-lock-timeout=200s", + "-state=teststate", + "-state-out=teststateout", + "-var-file=testvarfile", + "-lock=false", + "-parallelism=99", + "-refresh=false", + "-target=target1", + "-target=target2", + "-var", "var1=foo", + "-var", "var2=bar", + "testfile", + }, nil, applyCmd) + }) } diff --git a/tfexec/cmd.go b/tfexec/cmd.go new file mode 100644 index 00000000..acfa46b2 --- /dev/null +++ b/tfexec/cmd.go @@ -0,0 +1,103 @@ +package tfexec + +import ( + "context" + "os" + "os/exec" + "strings" +) + +const ( + checkpointDisableEnvVar = "CHECKPOINT_DISABLE" + logEnvVar = "TF_LOG" + inputEnvVar = "TF_INPUT" + automationEnvVar = "TF_IN_AUTOMATION" + logPathEnvVar = "TF_LOG_PATH" + reattachEnvVar = "TF_REATTACH_PROVIDERS" + + varEnvVarPrefix = "TF_VAR_" +) + +var prohibitedEnvVars = []string{ + inputEnvVar, + automationEnvVar, + logPathEnvVar, + logEnvVar, + reattachEnvVar, +} + +func envMap(environ []string) map[string]string { + env := map[string]string{} + for _, ev := range environ { + parts := strings.SplitN(ev, "=", 2) + if len(parts) == 0 { + continue + } + k := parts[0] + v := "" + if len(parts) == 2 { + v = parts[1] + } + env[k] = v + } + return env +} + +func envSlice(environ map[string]string) []string { + env := []string{} + for k, v := range environ { + env = append(env, k+"="+v) + } + return env +} + +func (tf *Terraform) buildEnv(mergeEnv map[string]string) []string { + // set Terraform level env, if env is nil, fall back to os.Environ + var env map[string]string + if tf.env == nil { + env = envMap(os.Environ()) + } else { + env = make(map[string]string, len(tf.env)) + for k, v := range tf.env { + env[k] = v + } + } + + // override env with any command specific environment + for k, v := range mergeEnv { + env[k] = v + } + + // always propagate CHECKPOINT_DISABLE env var unless it is + // explicitly overridden with tf.SetEnv or command env + if _, ok := env[checkpointDisableEnvVar]; !ok { + env[checkpointDisableEnvVar] = os.Getenv(checkpointDisableEnvVar) + } + + // always override logging + if tf.logPath == "" { + // so logging can't pollute our stderr output + env[logEnvVar] = "" + env[logPathEnvVar] = "" + } else { + env[logPathEnvVar] = tf.logPath + // Log levels other than TRACE are currently unreliable, the CLI recommends using TRACE only. + env[logEnvVar] = "TRACE" + } + + // constant automation override env vars + env[inputEnvVar] = "0" + env[automationEnvVar] = "1" + + return envSlice(env) +} + +func (tf *Terraform) buildTerraformCmd(ctx context.Context, args ...string) *exec.Cmd { + cmd := exec.CommandContext(ctx, tf.execPath, args...) + cmd.Env = tf.buildEnv(nil) + cmd.Dir = tf.workingDir + + tf.logger.Printf("[INFO] running Terraform command: %s", cmdString(cmd)) + + return cmd +} diff --git a/tfexec/cmd_test.go b/tfexec/cmd_test.go new file mode 100644 index 00000000..b527da9e --- /dev/null +++ b/tfexec/cmd_test.go @@ -0,0 +1,51 @@ +package tfexec + +import ( + "os/exec" + "strings" + "testing" +) + +var defaultEnv = []string{ + "TF_LOG=", + "TF_LOG_PATH=", + "TF_INPUT=0", + "TF_IN_AUTOMATION=1", + "CHECKPOINT_DISABLE=", +} + +func assertCmd(t *testing.T, expectedArgs []string, expectedEnv map[string]string, actual *exec.Cmd) { + t.Helper() + + // check args (skip path) + actualArgs := actual.Args[1:] + + if len(expectedArgs) != len(actualArgs) { + t.Fatalf("args mismatch\n\nexpected:\n%v\n\ngot:\n%v", strings.Join(expectedArgs, " "), strings.Join(actualArgs, " ")) + } + for i := range expectedArgs { + if expectedArgs[i] != actualArgs[i] { + t.Fatalf("args mismatch, expected %q, got %q\n\nfull expected:\n%v\n\nfull actual:\n%v", expectedArgs[i], actualArgs[i], strings.Join(expectedArgs, " "), strings.Join(actualArgs, " ")) + } + } + + // check environment + expectedEnv = envMap(append(defaultEnv, envSlice(expectedEnv)...)) + + // compare against raw slice len incase of duplication or something + if len(expectedEnv) != len(actual.Env) { + t.Fatalf("env mismatch\n\nexpected:\n%v\n\ngot:\n%v", envSlice(expectedEnv), actual.Env) + } + + actualEnv := envMap(actual.Env) + + for k, ev := range expectedEnv { + av, ok := actualEnv[k] + if !ok { + t.Fatalf("env mismatch, missing %q\n\nfull expected:\n%v\n\nfull actual:\n%v", k, envSlice(expectedEnv), envSlice(actualEnv)) + } + if ev != av { + t.Fatalf("env mismatch, expected %q, got %q\n\nfull expected:\n%v\n\nfull actual:\n%v", ev, av, envSlice(expectedEnv), envSlice(actualEnv)) + } + } +} diff --git a/tfexec/destroy.go b/tfexec/destroy.go index 70cbf7a7..b2b07611 100644 --- a/tfexec/destroy.go +++ b/tfexec/destroy.go @@ -129,7 +129,7 @@ func (tf *Terraform) destroyCmd(ctx context.Context, opts ...DestroyOption) *exe } if c.vars != nil { for _, v := range c.vars { - args = append(args, "-var '"+v+"'") + args = append(args, "-var", v) } } diff --git a/tfexec/destroy_test.go b/tfexec/destroy_test.go index d6635ba9..85738b09 100644 --- a/tfexec/destroy_test.go +++ b/tfexec/destroy_test.go @@ -3,7 +3,6 @@ package tfexec import ( "context" "os" - "strings" "testing" ) @@ -16,25 +15,42 @@ func TestDestroyCmd(t *testing.T) { t.Fatal(err) } - // defaults - destroyCmd := tf.destroyCmd(context.Background()) - - actual := strings.TrimPrefix(cmdString(destroyCmd), destroyCmd.Path+" ") - - expected := "destroy -no-color -auto-approve -lock-timeout=0s -lock=true -parallelism=10 -refresh=true" - - if actual != expected { - t.Fatalf("expected default arguments of DestroyCmd:\n%s\n actual arguments:\n%s\n", expected, actual) - } - - // override all defaults - destroyCmd = tf.destroyCmd(context.Background(), Backup("testbackup"), LockTimeout("200s"), State("teststate"), StateOut("teststateout"), VarFile("testvarfile"), Lock(false), Parallelism(99), Refresh(false), Target("target1"), Target("target2"), Var("var1=foo"), Var("var2=bar")) - - actual = strings.TrimPrefix(cmdString(destroyCmd), destroyCmd.Path+" ") - - expected = "destroy -no-color -auto-approve -backup=testbackup -lock-timeout=200s -state=teststate -state-out=teststateout -var-file=testvarfile -lock=false -parallelism=99 -refresh=false -target=target1 -target=target2 -var 'var1=foo' -var 'var2=bar'" - - if actual != expected { - t.Fatalf("expected arguments of DestroyCmd:\n%s\n actual arguments:\n%s\n", expected, actual) - } + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) + + t.Run("defaults", func(t *testing.T) { + destroyCmd := tf.destroyCmd(context.Background()) + + assertCmd(t, []string{ + "destroy", + "-no-color", + "-auto-approve", + "-lock-timeout=0s", + "-lock=true", + "-parallelism=10", + "-refresh=true", + }, nil, destroyCmd) + }) + + t.Run("override all defaults", func(t *testing.T) { + destroyCmd := tf.destroyCmd(context.Background(), Backup("testbackup"), LockTimeout("200s"), State("teststate"), StateOut("teststateout"), VarFile("testvarfile"), Lock(false), Parallelism(99), Refresh(false), Target("target1"), Target("target2"), Var("var1=foo"), Var("var2=bar")) + + assertCmd(t, []string{ + "destroy", + "-no-color", + "-auto-approve", + "-backup=testbackup", + "-lock-timeout=200s", + "-state=teststate", + "-state-out=teststateout", + "-var-file=testvarfile", + "-lock=false", + "-parallelism=99", + "-refresh=false", + "-target=target1", + "-target=target2", + "-var", "var1=foo", + "-var", "var2=bar", + }, nil, destroyCmd) + }) } diff --git a/tfexec/import.go b/tfexec/import.go index 4fba28dd..5d715ec9 100644 --- a/tfexec/import.go +++ b/tfexec/import.go @@ -121,7 +121,7 @@ func (tf *Terraform) importCmd(ctx context.Context, address, id string, opts ... // string slice opts: split into separate args if c.vars != nil { for _, v := range c.vars { - args = append(args, "-var '"+v+"'") + args = append(args, "-var", v) } } diff --git a/tfexec/import_test.go b/tfexec/import_test.go index ebb71e24..d34601ae 100644 --- a/tfexec/import_test.go +++ b/tfexec/import_test.go @@ -91,35 +91,49 @@ func TestImportCmd(t *testing.T) { t.Fatal(err) } - // defaults - importCmd := tf.importCmd(context.Background(), "my-addr", "my-id") - - actual := strings.TrimPrefix(cmdString(importCmd), importCmd.Path+" ") - - expected := "import -no-color -lock-timeout=0s -lock=true my-addr my-id" - - if actual != expected { - t.Fatalf("expected default arguments of ImportCmd:\n%s\n actual arguments:\n%s\n", expected, actual) - } - - // override all defaults - importCmd = tf.importCmd(context.Background(), "my-addr2", "my-id2", - Backup("testbackup"), - LockTimeout("200s"), - State("teststate"), - StateOut("teststateout"), - VarFile("testvarfile"), - Lock(false), - Var("var1=foo"), - Var("var2=bar"), - AllowMissingConfig(true), - ) - - actual = strings.TrimPrefix(cmdString(importCmd), importCmd.Path+" ") - - expected = "import -no-color -backup=testbackup -lock-timeout=200s -state=teststate -state-out=teststateout -var-file=testvarfile -lock=false -allow-missing-config -var 'var1=foo' -var 'var2=bar' my-addr2 my-id2" - - if actual != expected { - t.Fatalf("expected arguments of ImportCmd:\n%s\n actual arguments:\n%s\n", expected, actual) - } + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) + + t.Run("defaults", func(t *testing.T) { + importCmd := tf.importCmd(context.Background(), "my-addr", "my-id") + + assertCmd(t, []string{ + "import", + "-no-color", + "-lock-timeout=0s", + "-lock=true", + "my-addr", + "my-id", + }, nil, importCmd) + }) + + t.Run("override all defaults", func(t *testing.T) { + importCmd := tf.importCmd(context.Background(), "my-addr2", "my-id2", + Backup("testbackup"), + LockTimeout("200s"), + State("teststate"), + StateOut("teststateout"), + VarFile("testvarfile"), + Lock(false), + Var("var1=foo"), + Var("var2=bar"), + AllowMissingConfig(true), + ) + + assertCmd(t, []string{ + "import", + "-no-color", + "-backup=testbackup", + "-lock-timeout=200s", + "-state=teststate", + "-state-out=teststateout", + "-var-file=testvarfile", + "-lock=false", + "-allow-missing-config", + "-var", "var1=foo", + "-var", "var2=bar", + "my-addr2", + "my-id2", + }, nil, importCmd) + }) } diff --git a/tfexec/init_test.go b/tfexec/init_test.go index 7e5c54ba..bfd74e3f 100644 --- a/tfexec/init_test.go +++ b/tfexec/init_test.go @@ -3,7 +3,6 @@ package tfexec import ( "context" "os" - "strings" "testing" ) @@ -16,25 +15,50 @@ func TestInitCmd(t *testing.T) { t.Fatal(err) } - // defaults - initCmd := tf.initCmd(context.Background()) + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) - actual := strings.TrimPrefix(cmdString(initCmd), initCmd.Path+" ") + t.Run("defaults", func(t *testing.T) { + // defaults + initCmd := tf.initCmd(context.Background()) + if err != nil { + t.Fatal(err) + } - expected := "init -no-color -force-copy -lock-timeout=0s -backend=true -get=true -get-plugins=true -lock=true -upgrade=false -verify-plugins=true" + assertCmd(t, []string{ + "init", + "-no-color", + "-force-copy", + "-lock-timeout=0s", + "-backend=true", + "-get=true", + "-get-plugins=true", + "-lock=true", + "-upgrade=false", + "-verify-plugins=true", + }, nil, initCmd) + }) - if actual != expected { - t.Fatalf("expected default arguments of InitCmd:\n%s\n actual arguments:\n%s\n", expected, actual) - } - - // override all defaults - initCmd = tf.initCmd(context.Background(), Backend(false), BackendConfig("confpath1"), BackendConfig("confpath2"), FromModule("testsource"), Get(false), GetPlugins(false), Lock(false), LockTimeout("999s"), PluginDir("testdir1"), PluginDir("testdir2"), Reconfigure(true), Upgrade(true), VerifyPlugins(false)) - - actual = strings.TrimPrefix(cmdString(initCmd), initCmd.Path+" ") + t.Run("override all defaults", func(t *testing.T) { + initCmd := tf.initCmd(context.Background(), Backend(false), BackendConfig("confpath1"), BackendConfig("confpath2"), FromModule("testsource"), Get(false), GetPlugins(false), Lock(false), LockTimeout("999s"), PluginDir("testdir1"), PluginDir("testdir2"), Reconfigure(true), Upgrade(true), VerifyPlugins(false)) - expected = "init -no-color -force-copy -from-module=testsource -lock-timeout=999s -backend=false -get=false -get-plugins=false -lock=false -upgrade=true -verify-plugins=false -reconfigure -backend-config=confpath1 -backend-config=confpath2 -plugin-dir=testdir1 -plugin-dir=testdir2" - - if actual != expected { - t.Fatalf("expected arguments of InitCmd:\n%s\n actual arguments:\n%s\n", expected, actual) - } + assertCmd(t, []string{ + "init", + "-no-color", + "-force-copy", + "-from-module=testsource", + "-lock-timeout=999s", + "-backend=false", + "-get=false", + "-get-plugins=false", + "-lock=false", + "-upgrade=true", + "-verify-plugins=false", + "-reconfigure", + "-backend-config=confpath1", + "-backend-config=confpath2", + "-plugin-dir=testdir1", + "-plugin-dir=testdir2", + }, nil, initCmd) + }) } diff --git a/tfexec/output_test.go b/tfexec/output_test.go index 8df68169..2d786240 100644 --- a/tfexec/output_test.go +++ b/tfexec/output_test.go @@ -3,7 +3,6 @@ package tfexec import ( "context" "os" - "strings" "testing" ) @@ -16,26 +15,28 @@ func TestOutputCmd(t *testing.T) { t.Fatal(err) } - // defaults - outputCmd := tf.outputCmd(context.Background()) - - actual := strings.TrimPrefix(cmdString(outputCmd), outputCmd.Path+" ") - - expected := "output -no-color -json" - - if actual != expected { - t.Fatalf("expected default arguments of OutputCmd:\n%s\n actual arguments:\n%s\n", expected, actual) - } - - // override all defaults - outputCmd = tf.outputCmd(context.Background(), - State("teststate")) - - actual = strings.TrimPrefix(cmdString(outputCmd), outputCmd.Path+" ") - - expected = "output -no-color -json -state=teststate" - - if actual != expected { - t.Fatalf("expected arguments of ImportCmd:\n%s\n actual arguments:\n%s\n", expected, actual) - } + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) + + t.Run("defaults", func(t *testing.T) { + outputCmd := tf.outputCmd(context.Background()) + + assertCmd(t, []string{ + "output", + "-no-color", + "-json", + }, nil, outputCmd) + }) + + t.Run("override all defaults", func(t *testing.T) { + outputCmd := tf.outputCmd(context.Background(), + State("teststate")) + + assertCmd(t, []string{ + "output", + "-no-color", + "-json", + "-state=teststate", + }, nil, outputCmd) + }) } diff --git a/tfexec/plan.go b/tfexec/plan.go index 678c38a0..3ac128cb 100644 --- a/tfexec/plan.go +++ b/tfexec/plan.go @@ -128,7 +128,7 @@ func (tf *Terraform) planCmd(ctx context.Context, opts ...PlanOption) *exec.Cmd } if c.vars != nil { for _, v := range c.vars { - args = append(args, "-var '"+v+"'") + args = append(args, "-var", v) } } diff --git a/tfexec/plan_test.go b/tfexec/plan_test.go index 922abb1b..12e5b7df 100644 --- a/tfexec/plan_test.go +++ b/tfexec/plan_test.go @@ -3,7 +3,6 @@ package tfexec import ( "context" "os" - "strings" "testing" ) @@ -16,25 +15,40 @@ func TestPlanCmd(t *testing.T) { t.Fatal(err) } - // defaults - planCmd := tf.planCmd(context.Background()) - - actual := strings.TrimPrefix(cmdString(planCmd), planCmd.Path+" ") - - expected := "plan -no-color -lock-timeout=0s -lock=true -parallelism=10 -refresh=true" - - if actual != expected { - t.Fatalf("expected default arguments of PlanCmd:\n%s\n actual arguments:\n%s\n", expected, actual) - } - - // override all defaults - planCmd = tf.planCmd(context.Background(), Destroy(true), Lock(false), LockTimeout("22s"), Out("whale"), Parallelism(42), Refresh(false), State("marvin"), Target("zaphod"), Target("beeblebrox"), Var("android=paranoid"), Var("brain_size=planet"), VarFile("trillian")) - - actual = strings.TrimPrefix(cmdString(planCmd), planCmd.Path+" ") - - expected = "plan -no-color -lock-timeout=22s -out=whale -state=marvin -var-file=trillian -lock=false -parallelism=42 -refresh=false -destroy -target=zaphod -target=beeblebrox -var 'android=paranoid' -var 'brain_size=planet'" - - if actual != expected { - t.Fatalf("expected arguments of PlanCmd:\n%s\n actual arguments:\n%s\n", expected, actual) - } + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) + + t.Run("defaults", func(t *testing.T) { + planCmd := tf.planCmd(context.Background()) + + assertCmd(t, []string{ + "plan", + "-no-color", + "-lock-timeout=0s", + "-lock=true", + "-parallelism=10", + "-refresh=true", + }, nil, planCmd) + }) + + t.Run("override all defaults", func(t *testing.T) { + planCmd := tf.planCmd(context.Background(), Destroy(true), Lock(false), LockTimeout("22s"), Out("whale"), Parallelism(42), Refresh(false), State("marvin"), Target("zaphod"), Target("beeblebrox"), Var("android=paranoid"), Var("brain_size=planet"), VarFile("trillian")) + + assertCmd(t, []string{ + "plan", + "-no-color", + "-lock-timeout=22s", + "-out=whale", + "-state=marvin", + "-var-file=trillian", + "-lock=false", + "-parallelism=42", + "-refresh=false", + "-destroy", + "-target=zaphod", + "-target=beeblebrox", + "-var", "android=paranoid", + "-var", "brain_size=planet", + }, nil, planCmd) + }) } diff --git a/tfexec/providers_schema_test.go b/tfexec/providers_schema_test.go index 5215ac06..9f16934e 100644 --- a/tfexec/providers_schema_test.go +++ b/tfexec/providers_schema_test.go @@ -3,7 +3,6 @@ package tfexec import ( "context" "os" - "strings" "testing" ) @@ -16,14 +15,15 @@ func TestProvidersSchemaCmd(t *testing.T) { t.Fatal(err) } - // defaults - schemaCmd := tf.providersSchemaCmd(context.Background()) - - actual := strings.TrimPrefix(cmdString(schemaCmd), schemaCmd.Path+" ") + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) - expected := "providers schema -json -no-color" + schemaCmd := tf.providersSchemaCmd(context.Background()) - if actual != expected { - t.Fatalf("expected default arguments of ProvidersSchemaCmd:\n%s\n actual arguments:\n%s\n", expected, actual) - } + assertCmd(t, []string{ + "providers", + "schema", + "-json", + "-no-color", + }, nil, schemaCmd) } diff --git a/tfexec/show_test.go b/tfexec/show_test.go index 6fc65e4a..87f3882f 100644 --- a/tfexec/show_test.go +++ b/tfexec/show_test.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" "reflect" - "strings" "testing" "github.com/davecgh/go-spew/spew" @@ -22,7 +21,7 @@ func TestShow(t *testing.T) { } // copy state and config files into test dir - err = copyFiles(filepath.Join(testFixtureDir, "basic"), td) + err = copyFiles(filepath.Join(testFixtureDir, "state"), td) if err != nil { t.Fatalf("error copying files into test dir: %s", err) } @@ -71,7 +70,7 @@ func TestShow_errInitRequired(t *testing.T) { t.Fatal(err) } - err = copyFile(filepath.Join(testFixtureDir, "basic", testTerraformStateFileName), td) + err = copyFiles(filepath.Join(testFixtureDir, "basic"), td) _, err = tf.Show(context.Background()) if err == nil { @@ -93,14 +92,15 @@ func TestShowCmd(t *testing.T) { t.Fatal(err) } + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) + // defaults showCmd := tf.showCmd(context.Background()) - actual := strings.TrimPrefix(cmdString(showCmd), showCmd.Path+" ") - - expected := "show -json -no-color" - - if actual != expected { - t.Fatalf("expected default arguments of ShowCmd:\n%s\n actual arguments:\n%s\n", expected, actual) - } + assertCmd(t, []string{ + "show", + "-json", + "-no-color", + }, nil, showCmd) } diff --git a/tfexec/terraform_cmd.go b/tfexec/terraform_cmd.go deleted file mode 100644 index 5ae787aa..00000000 --- a/tfexec/terraform_cmd.go +++ /dev/null @@ -1,93 +0,0 @@ -package tfexec - -import ( - "context" - "log" - "os" - "os/exec" - "strings" -) - -const ( - checkpointDisableEnvVar = "CHECKPOINT_DISABLE" - logEnvVar = "TF_LOG" - inputEnvVar = "TF_INPUT" - automationEnvVar = "TF_IN_AUTOMATION" - logPathEnvVar = "TF_LOG_PATH" - - varEnvVarPrefix = "TF_VAR_" -) - -var prohibitedEnvVars = []string{ - inputEnvVar, - automationEnvVar, - logPathEnvVar, - logEnvVar, -} - -func environ() map[string]string { - env := map[string]string{} - for _, ev := range os.Environ() { - parts := strings.SplitN(ev, "=", 2) - if len(parts) == 0 { - continue - } - k := parts[0] - v := "" - if len(parts) == 2 { - v = parts[1] - } - env[k] = v - } - return env -} - -func (tf *Terraform) buildEnv() []string { - var menv map[string]string - if tf.env == nil { - menv = environ() - } else { - menv = make(map[string]string, len(tf.env)) - for k, v := range tf.env { - menv[k] = v - } - } - - if _, ok := menv[checkpointDisableEnvVar]; !ok { - // always propagate CHECKPOINT_DISABLE env var unless it is - // explicitly overridden with tf.SetEnv - menv[checkpointDisableEnvVar] = os.Getenv(checkpointDisableEnvVar) - } - - if tf.logPath == "" { - // so logging can't pollute our stderr output - menv[logEnvVar] = "" - menv[logPathEnvVar] = "" - } else { - menv[logPathEnvVar] = tf.logPath - // Log levels other than TRACE are currently unreliable, the CLI recommends using TRACE only. - menv[logEnvVar] = "TRACE" - } - - menv[inputEnvVar] = "0" - menv[automationEnvVar] = "1" - - env := []string{} - for k, v := range menv { - env = append(env, k+"="+v) - } - - return env -} - -func (tf *Terraform) buildTerraformCmd(ctx context.Context, args ...string) *exec.Cmd { - env := tf.buildEnv() - - cmd := exec.CommandContext(ctx, tf.execPath, args...) - cmd.Env = env - cmd.Dir = tf.workingDir - - log.Printf("[INFO] running Terraform command: %s", cmdString(cmd)) - - return cmd -} diff --git a/tfexec/terraform_test.go b/tfexec/terraform_test.go index 7eb2dea1..8be76334 100644 --- a/tfexec/terraform_test.go +++ b/tfexec/terraform_test.go @@ -7,8 +7,6 @@ import ( "io/ioutil" "os" "path/filepath" - "reflect" - "sort" "sync" "testing" @@ -75,43 +73,67 @@ func TestCheckpointDisablePropagation(t *testing.T) { t.Fatal(err) } - // case 1: env var is set in environment and not overridden err = os.Setenv("CHECKPOINT_DISABLE", "1") if err != nil { t.Fatal(err) } defer os.Unsetenv("CHECKPOINT_DISABLE") - tf.SetEnv(map[string]string{ - "FOOBAR": "1", + t.Run("case 1: env var is set in environment and not overridden", func(t *testing.T) { + + err = tf.SetEnv(map[string]string{ + "FOOBAR": "1", + }) + if err != nil { + t.Fatal(err) + } + + initCmd := tf.initCmd(context.Background()) + + assertCmd(t, []string{ + "init", + "-no-color", + "-force-copy", + "-lock-timeout=0s", + "-backend=true", + "-get=true", + "-get-plugins=true", + "-lock=true", + "-upgrade=false", + "-verify-plugins=true", + }, map[string]string{ + "CHECKPOINT_DISABLE": "1", + "FOOBAR": "1", + }, initCmd) }) - initCmd := tf.initCmd(context.Background()) - expected := []string{"CHECKPOINT_DISABLE=1", "FOOBAR=1", "TF_INPUT=0", "TF_IN_AUTOMATION=1", "TF_LOG=", "TF_LOG_PATH="} - s := initCmd.Env - sort.Strings(s) - actual := s - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("expected command env to be %s, but it was %s", expected, actual) - } - // case 2: env var is set in environment and overridden with SetEnv - err = tf.SetEnv(map[string]string{ - "CHECKPOINT_DISABLE": "", - "FOOBAR": "1", + t.Run("case 2: env var is set in environment and overridden with SetEnv", func(t *testing.T) { + err = tf.SetEnv(map[string]string{ + "CHECKPOINT_DISABLE": "", + "FOOBAR": "2", + }) + if err != nil { + t.Fatal(err) + } + + initCmd := tf.initCmd(context.Background()) + + assertCmd(t, []string{ + "init", + "-no-color", + "-force-copy", + "-lock-timeout=0s", + "-backend=true", + "-get=true", + "-get-plugins=true", + "-lock=true", + "-upgrade=false", + "-verify-plugins=true", + }, map[string]string{ + "CHECKPOINT_DISABLE": "", + "FOOBAR": "2", + }, initCmd) }) - if err != nil { - t.Fatal(err) - } - initCmd = tf.initCmd(context.Background()) - expected = []string{"CHECKPOINT_DISABLE=", "FOOBAR=1", "TF_INPUT=0", "TF_IN_AUTOMATION=1", "TF_LOG=", "TF_LOG_PATH="} - s = initCmd.Env - sort.Strings(s) - actual = s - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("expected command env to be %s, but it was %s", expected, actual) - } } func testTempDir(t *testing.T) string { diff --git a/tfexec/testdata/state/main.tf b/tfexec/testdata/state/main.tf new file mode 100644 index 00000000..fca4eb6d --- /dev/null +++ b/tfexec/testdata/state/main.tf @@ -0,0 +1,3 @@ +resource null_resource "foo" { +} + diff --git a/tfexec/testdata/basic/terraform.tfstate b/tfexec/testdata/state/terraform.tfstate similarity index 100% rename from tfexec/testdata/basic/terraform.tfstate rename to tfexec/testdata/state/terraform.tfstate diff --git a/tfexec/testdata/var/main.tf b/tfexec/testdata/var/main.tf new file mode 100644 index 00000000..6d043e16 --- /dev/null +++ b/tfexec/testdata/var/main.tf @@ -0,0 +1,8 @@ +variable "in" { + type = string + default = "default" +} + +output "out" { + value = var.in +}