Repository: gruntwork-io/terratest

Issue:
Terraform options does not allow to specify backend file
I want to give my users an option to select either local or remote backend. But it seems Terratest does not support setting path to the config file via `-backend-config=PATH` parameter, only key-value parameters. 

Hints from issue comments:
@yorinasub17: Seems like a useful thing to add. Would you be up for contributing a PR to implement this? I think a good way to do this is to output just the `key` and not `key=value` when `value` is `null` in this function https://github.com/gruntwork-io/terratest/blob/master/modules/terraform/format.go#L83@patryykm: Any update here? This feature will be very useful but PR was closed for some reason.

Solution patch:
diff --git a/modules/terraform/format.go b/modules/terraform/format.go
index 7c31bb43..83afb22f 100644
--- a/modules/terraform/format.go
+++ b/modules/terraform/format.go
@@ -104,7 +104,7 @@ func FormatTerraformPlanFileAsArg(commandType string, outPath string) []string {
 // FormatTerraformVarsAsArgs formats the given variables as command-line args for Terraform (e.g. of the format
 // -var key=value).
 func FormatTerraformVarsAsArgs(vars map[string]interface{}) []string {
-	return formatTerraformArgs(vars, "-var", true)
+	return formatTerraformArgs(vars, "-var", true, false)
 }
 
 // FormatTerraformLockAsArgs formats the lock and lock-timeout variables
@@ -141,18 +141,25 @@ func FormatTerraformArgs(argName string, args []string) []string {
 // FormatTerraformBackendConfigAsArgs formats the given variables as backend config args for Terraform (e.g. of the
 // format -backend-config=key=value).
 func FormatTerraformBackendConfigAsArgs(vars map[string]interface{}) []string {
-	return formatTerraformArgs(vars, "-backend-config", false)
+	return formatTerraformArgs(vars, "-backend-config", false, true)
 }
 
 // Format the given vars into 'Terraform' format, with each var being prefixed with the given prefix. If
 // useSpaceAsSeparator is true, a space will separate the prefix and each var (e.g., -var foo=bar). If
-// useSpaceAsSeparator is false, an equals will separate the prefix and each var (e.g., -backend-config=foo=bar).
-func formatTerraformArgs(vars map[string]interface{}, prefix string, useSpaceAsSeparator bool) []string {
+// useSpaceAsSeparator is false, an equals will separate the prefix and each var (e.g., -backend-config=foo=bar). If
+// omitNil is false, then nil values will be included, (e.g. -backend-config=foo=null). If
+// omitNil is true, then nil values will not be included, (e.g. -backend-config=foo). If
+func formatTerraformArgs(vars map[string]interface{}, prefix string, useSpaceAsSeparator bool, omitNil bool) []string {
 	var args []string
 
 	for key, value := range vars {
-		hclString := toHclString(value, false)
-		argValue := fmt.Sprintf("%s=%s", key, hclString)
+		var argValue string
+		if omitNil && value == nil {
+			argValue = key
+		} else {
+			hclString := toHclString(value, false)
+			argValue = fmt.Sprintf("%s=%s", key, hclString)
+		}
 		if useSpaceAsSeparator {
 			args = append(args, prefix, argValue)
 		} else {
diff --git a/modules/terraform/options.go b/modules/terraform/options.go
index 3e0eaf92..c5eb9c3b 100644
--- a/modules/terraform/options.go
+++ b/modules/terraform/options.go
@@ -56,7 +56,7 @@ type Options struct {
 	Lock                     bool                   // The lock option to pass to the terraform command with -lock
 	LockTimeout              string                 // The lock timeout option to pass to the terraform command with -lock-timeout
 	EnvVars                  map[string]string      // Environment variables to set when running Terraform
-	BackendConfig            map[string]interface{} // The vars to pass to the terraform init command for extra configuration for the backend
+	BackendConfig            map[string]interface{} // The vars to pass to the terraform init command for extra configuration for the backend. If a var is nil, it will be formated as `--backend-config=var` instead of `--backend-config=var=null`
 	RetryableTerraformErrors map[string]string      // If Terraform apply fails with one of these (transient) errors, retry. The keys are a regexp to match against the error and the message is what to display to a user if that error is matched.
 	MaxRetries               int                    // Maximum number of times to retry errors matching RetryableTerraformErrors
 	TimeBetweenRetries       time.Duration          // The amount of time to wait between retries
diff --git a/modules/terraform/var.go b/modules/terraform/var.go
index 96434555..ac057441 100644
--- a/modules/terraform/var.go
+++ b/modules/terraform/var.go
@@ -16,7 +16,7 @@ type varInline struct {
 
 func (vi varInline) Args() []string {
 	m := map[string]interface{}{vi.name: vi.value}
-	return formatTerraformArgs(m, "-var", true)
+	return formatTerraformArgs(m, "-var", true, false)
 }
 func (vi varInline) internal() {}
 



Test patch:
diff --git a/modules/terraform/init_test.go b/modules/terraform/init_test.go
index 3198c523..ca5da8b8 100644
--- a/modules/terraform/init_test.go
+++ b/modules/terraform/init_test.go
@@ -13,25 +13,47 @@ import (
 func TestInitBackendConfig(t *testing.T) {
 	t.Parallel()
 
-	stateDirectory := t.TempDir()
-
-	remoteStateFile := filepath.Join(stateDirectory, "backend.tfstate")
-
-	testFolder, err := files.CopyTerraformFolderToTemp("../../test/fixtures/terraform-backend", t.Name())
+	testFolderPath := "../../test/fixtures/terraform-backend"
+	testFolder, err := files.CopyTerraformFolderToTemp(testFolderPath, t.Name())
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	options := &Options{
-		TerraformDir: testFolder,
-		BackendConfig: map[string]interface{}{
-			"path": remoteStateFile,
+	tmpStateFile := filepath.Join(t.TempDir(), "backend.tfstate")
+	ttable := []struct {
+		name    string
+		path    string
+		options *Options
+	}{
+		{
+			name: "KeyValue",
+			path: tmpStateFile,
+			options: &Options{
+				TerraformDir: testFolder,
+				BackendConfig: map[string]interface{}{
+					"path": tmpStateFile,
+				},
+			},
+		},
+		{
+			name: "File",
+			path: filepath.Join(testFolder, "backend.tfstate"),
+			options: &Options{
+				TerraformDir: testFolder,
+				Reconfigure:  true,
+				BackendConfig: map[string]interface{}{
+					"backend.hcl": nil,
+				},
+			},
 		},
 	}
 
-	InitAndApply(t, options)
-
-	assert.FileExists(t, remoteStateFile)
+	for _, tt := range ttable {
+		t.Run(tt.name, func(t *testing.T) {
+			InitAndApply(t, tt.options)
+			assert.FileExists(t, tt.path)
+		})
+	}
 }
 
 func TestInitPluginDir(t *testing.T) {
diff --git a/test/fixtures/terraform-backend/backend.hcl b/test/fixtures/terraform-backend/backend.hcl
new file mode 100644
index 00000000..9d0fbd2c
--- /dev/null
+++ b/test/fixtures/terraform-backend/backend.hcl
@@ -0,0 +1 @@
+path="backend.tfstate"
\ No newline at end of file