From 17383ebdd5a1bd1cf7b6a160f530d4a4e201e522 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 1 Jul 2022 05:20:14 -0400 Subject: [PATCH] tfexec: Add (Terraform).SetLogCore() and (Terraform).SetLogProvider() methods (#324) Reference: https://github.com/hashicorp/terraform-exec/issues/101 --- tfexec/cmd.go | 14 +- tfexec/cmd_test.go | 4 +- tfexec/terraform.go | 41 +++- tfexec/terraform_test.go | 431 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 468 insertions(+), 22 deletions(-) diff --git a/tfexec/cmd.go b/tfexec/cmd.go index df7cd499..56393a00 100644 --- a/tfexec/cmd.go +++ b/tfexec/cmd.go @@ -19,10 +19,12 @@ import ( const ( checkpointDisableEnvVar = "CHECKPOINT_DISABLE" cliArgsEnvVar = "TF_CLI_ARGS" - logEnvVar = "TF_LOG" inputEnvVar = "TF_INPUT" automationEnvVar = "TF_IN_AUTOMATION" + logEnvVar = "TF_LOG" + logCoreEnvVar = "TF_LOG_CORE" logPathEnvVar = "TF_LOG_PATH" + logProviderEnvVar = "TF_LOG_PROVIDER" reattachEnvVar = "TF_REATTACH_PROVIDERS" appendUserAgentEnvVar = "TF_APPEND_USER_AGENT" workspaceEnvVar = "TF_WORKSPACE" @@ -37,8 +39,10 @@ var prohibitedEnvVars = []string{ cliArgsEnvVar, inputEnvVar, automationEnvVar, - logPathEnvVar, logEnvVar, + logCoreEnvVar, + logPathEnvVar, + logProviderEnvVar, reattachEnvVar, appendUserAgentEnvVar, workspaceEnvVar, @@ -148,10 +152,14 @@ func (tf *Terraform) buildEnv(mergeEnv map[string]string) []string { if tf.logPath == "" { // so logging can't pollute our stderr output env[logEnvVar] = "" + env[logCoreEnvVar] = "" env[logPathEnvVar] = "" + env[logProviderEnvVar] = "" } else { - env[logPathEnvVar] = tf.logPath env[logEnvVar] = tf.log + env[logCoreEnvVar] = tf.logCore + env[logPathEnvVar] = tf.logPath + env[logProviderEnvVar] = tf.logProvider } // constant automation override env vars diff --git a/tfexec/cmd_test.go b/tfexec/cmd_test.go index 3f283208..d345e859 100644 --- a/tfexec/cmd_test.go +++ b/tfexec/cmd_test.go @@ -39,8 +39,10 @@ func defaultEnv() []string { "CHECKPOINT_DISABLE=", "TF_APPEND_USER_AGENT=HashiCorp-terraform-exec/" + version.ModuleVersion(), "TF_IN_AUTOMATION=1", - "TF_LOG_PATH=", "TF_LOG=", + "TF_LOG_CORE=", + "TF_LOG_PATH=", + "TF_LOG_PROVIDER=", "TF_WORKSPACE=", } } diff --git a/tfexec/terraform.go b/tfexec/terraform.go index 835e7e06..bb8be17d 100644 --- a/tfexec/terraform.go +++ b/tfexec/terraform.go @@ -55,9 +55,15 @@ type Terraform struct { // TF_LOG environment variable, defaults to TRACE if logPath is set. log string + // TF_LOG_CORE environment variable + logCore string + // TF_LOG_PATH environment variable logPath string + // TF_LOG_PROVIDER environment variable + logProvider string + versionLock sync.Mutex execVersion *version.Version provVersions map[string]*version.Version @@ -131,8 +137,10 @@ func (tf *Terraform) SetStderr(w io.Writer) { // This must be combined with a call to SetLogPath to take effect. // // This is only compatible with Terraform CLI 0.15.0 or later as setting the -// log level was unreliable in earlier versions. It will default to TRACE for -// those earlier versions when SetLogPath is called. +// log level was unreliable in earlier versions. It will default to TRACE when +// SetLogPath is called on versions 0.14.11 and earlier, or if SetLogCore and +// SetLogProvider have not been called before SetLogPath on versions 0.15.0 and +// later. func (tf *Terraform) SetLog(log string) error { err := tf.compatible(context.Background(), tf0_15_0, nil) if err != nil { @@ -142,17 +150,44 @@ func (tf *Terraform) SetLog(log string) error { return nil } +// SetLogCore sets the TF_LOG_CORE environment variable for Terraform CLI +// execution. This must be combined with a call to SetLogPath to take effect. +// +// This is only compatible with Terraform CLI 0.15.0 or later. +func (tf *Terraform) SetLogCore(logCore string) error { + err := tf.compatible(context.Background(), tf0_15_0, nil) + if err != nil { + return err + } + tf.logCore = logCore + return nil +} + // SetLogPath sets the TF_LOG_PATH environment variable for Terraform CLI // execution. func (tf *Terraform) SetLogPath(path string) error { tf.logPath = path // Prevent setting the log path without enabling logging - if tf.log == "" { + if tf.log == "" && tf.logCore == "" && tf.logProvider == "" { tf.log = "TRACE" } return nil } +// SetLogProvider sets the TF_LOG_PROVIDER environment variable for Terraform +// CLI execution. This must be combined with a call to SetLogPath to take +// effect. +// +// This is only compatible with Terraform CLI 0.15.0 or later. +func (tf *Terraform) SetLogProvider(logProvider string) error { + err := tf.compatible(context.Background(), tf0_15_0, nil) + if err != nil { + return err + } + tf.logProvider = logProvider + return nil +} + // SetAppendUserAgent sets the TF_APPEND_USER_AGENT environment variable for // Terraform CLI execution. func (tf *Terraform) SetAppendUserAgent(ua string) error { diff --git a/tfexec/terraform_test.go b/tfexec/terraform_test.go index 32b7da3d..7991991b 100644 --- a/tfexec/terraform_test.go +++ b/tfexec/terraform_test.go @@ -123,8 +123,11 @@ func TestSetLog(t *testing.T) { "-get=true", "-upgrade=false", }, map[string]string{ - "CLEARENV": "1", - "TF_LOG": "", + "CLEARENV": "1", + "TF_LOG": "", + "TF_LOG_CORE": "", + "TF_LOG_PATH": "", + "TF_LOG_PROVIDER": "", }, initCmd) }) @@ -158,9 +161,11 @@ func TestSetLog(t *testing.T) { "-get=true", "-upgrade=false", }, map[string]string{ - "CLEARENV": "1", - "TF_LOG": "TRACE", - "TF_LOG_PATH": tfLogPath, + "CLEARENV": "1", + "TF_LOG": "TRACE", + "TF_LOG_CORE": "", + "TF_LOG_PATH": tfLogPath, + "TF_LOG_PROVIDER": "", }, initCmd) }) @@ -194,9 +199,156 @@ func TestSetLog(t *testing.T) { "-get=true", "-upgrade=false", }, map[string]string{ - "CLEARENV": "1", - "TF_LOG": "DEBUG", - "TF_LOG_PATH": tfLogPath, + "CLEARENV": "1", + "TF_LOG": "DEBUG", + "TF_LOG_CORE": "", + "TF_LOG_PATH": tfLogPath, + "TF_LOG_PROVIDER": "", + }, initCmd) + }) +} + +func TestSetLogCore(t *testing.T) { + td := t.TempDir() + + tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1)) + + if err != nil { + t.Fatalf("unexpected NewTerraform error: %s", err) + } + + // Required so all testing environment variables are not copied. + err = tf.SetEnv(map[string]string{ + "CLEARENV": "1", + }) + + if err != nil { + t.Fatalf("unexpected SetEnv error: %s", err) + } + + t.Run("case 1: SetLogCore <= 0.15 error", func(t *testing.T) { + if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { + t.Skip("Terraform for darwin/arm64 is not available until v1") + } + + td012 := t.TempDir() + + tf012, err := NewTerraform(td012, tfVersion(t, testutil.Latest012)) + + if err != nil { + t.Fatalf("unexpected NewTerraform error: %s", err) + } + + err = tf012.SetLogCore("TRACE") + + if err == nil { + t.Fatal("expected SetLogCore error, got none") + } + }) + + t.Run("case 2: SetLogCore TRACE no SetLogPath", func(t *testing.T) { + err := tf.SetLogCore("TRACE") + + if err != nil { + t.Fatalf("unexpected SetLogCore error: %s", err) + } + + initCmd, err := tf.initCmd(context.Background()) + + if err != nil { + t.Fatalf("unexpected command error: %s", err) + } + + assertCmd(t, []string{ + "init", + "-no-color", + "-force-copy", + "-input=false", + "-backend=true", + "-get=true", + "-upgrade=false", + }, map[string]string{ + "CLEARENV": "1", + "TF_LOG": "", + "TF_LOG_CORE": "", + "TF_LOG_PATH": "", + "TF_LOG_PROVIDER": "", + }, initCmd) + }) + + t.Run("case 3: SetLogCore TRACE and SetLogPath", func(t *testing.T) { + tfLogPath := filepath.Join(td, "test.log") + + err := tf.SetLogCore("TRACE") + + if err != nil { + t.Fatalf("unexpected SetLogCore error: %s", err) + } + + err = tf.SetLogPath(tfLogPath) + + if err != nil { + t.Fatalf("unexpected SetLogPath error: %s", err) + } + + initCmd, err := tf.initCmd(context.Background()) + + if err != nil { + t.Fatalf("unexpected command error: %s", err) + } + + assertCmd(t, []string{ + "init", + "-no-color", + "-force-copy", + "-input=false", + "-backend=true", + "-get=true", + "-upgrade=false", + }, map[string]string{ + "CLEARENV": "1", + "TF_LOG": "", + "TF_LOG_CORE": "TRACE", + "TF_LOG_PATH": tfLogPath, + "TF_LOG_PROVIDER": "", + }, initCmd) + }) + + t.Run("case 4: SetLogCore DEBUG and SetLogPath", func(t *testing.T) { + tfLogPath := filepath.Join(td, "test.log") + + err := tf.SetLogCore("DEBUG") + + if err != nil { + t.Fatalf("unexpected SetLogCore error: %s", err) + } + + err = tf.SetLogPath(tfLogPath) + + if err != nil { + t.Fatalf("unexpected SetLogPath error: %s", err) + } + + initCmd, err := tf.initCmd(context.Background()) + + if err != nil { + t.Fatalf("unexpected command error: %s", err) + } + + assertCmd(t, []string{ + "init", + "-no-color", + "-force-copy", + "-input=false", + "-backend=true", + "-get=true", + "-upgrade=false", + }, map[string]string{ + "CLEARENV": "1", + "TF_LOG": "", + "TF_LOG_CORE": "DEBUG", + "TF_LOG_PATH": tfLogPath, + "TF_LOG_PROVIDER": "", }, initCmd) }) } @@ -235,15 +387,262 @@ func TestSetLogPath(t *testing.T) { "-get=true", "-upgrade=false", }, map[string]string{ - "CLEARENV": "1", - "TF_LOG": "", - "TF_LOG_PATH": "", + "CLEARENV": "1", + "TF_LOG": "", + "TF_LOG_CORE": "", + "TF_LOG_PATH": "", + "TF_LOG_PROVIDER": "", + }, initCmd) + }) + + t.Run("case 2: SetLogPath sets TF_LOG (if no TF_LOG_CORE or TF_LOG_PROVIDER) and TF_LOG_PATH", func(t *testing.T) { + tfLogPath := filepath.Join(td, "test.log") + + err = tf.SetLogPath(tfLogPath) + + if err != nil { + t.Fatalf("unexpected SetLogPath error: %s", err) + } + + initCmd, err := tf.initCmd(context.Background()) + + if err != nil { + t.Fatalf("unexpected command error: %s", err) + } + + assertCmd(t, []string{ + "init", + "-no-color", + "-force-copy", + "-input=false", + "-backend=true", + "-get=true", + "-upgrade=false", + }, map[string]string{ + "CLEARENV": "1", + "TF_LOG": "TRACE", + "TF_LOG_CORE": "", + "TF_LOG_PATH": tfLogPath, + "TF_LOG_PROVIDER": "", + }, initCmd) + }) + + t.Run("case 3: SetLogPath does not set TF_LOG if TF_LOG_CORE", func(t *testing.T) { + tfLogPath := filepath.Join(td, "test.log") + + err := tf.SetLog("") + + if err != nil { + t.Fatalf("unexpected SetLog error: %s", err) + } + + err = tf.SetLogCore("TRACE") + + if err != nil { + t.Fatalf("unexpected SetLogCore error: %s", err) + } + + err = tf.SetLogProvider("") + + if err != nil { + t.Fatalf("unexpected SetLogProvider error: %s", err) + } + + err = tf.SetLogPath(tfLogPath) + + if err != nil { + t.Fatalf("unexpected SetLogPath error: %s", err) + } + + initCmd, err := tf.initCmd(context.Background()) + + if err != nil { + t.Fatalf("unexpected command error: %s", err) + } + + assertCmd(t, []string{ + "init", + "-no-color", + "-force-copy", + "-input=false", + "-backend=true", + "-get=true", + "-upgrade=false", + }, map[string]string{ + "CLEARENV": "1", + "TF_LOG": "", + "TF_LOG_CORE": "TRACE", + "TF_LOG_PATH": tfLogPath, + "TF_LOG_PROVIDER": "", + }, initCmd) + }) + + t.Run("case 4: SetLogPath does not set TF_LOG if TF_LOG_PROVIDER", func(t *testing.T) { + tfLogPath := filepath.Join(td, "test.log") + + err := tf.SetLog("") + + if err != nil { + t.Fatalf("unexpected SetLog error: %s", err) + } + + err = tf.SetLogCore("") + + if err != nil { + t.Fatalf("unexpected SetLogCore error: %s", err) + } + + err = tf.SetLogProvider("TRACE") + + if err != nil { + t.Fatalf("unexpected SetLogProvider error: %s", err) + } + + err = tf.SetLogPath(tfLogPath) + + if err != nil { + t.Fatalf("unexpected SetLogPath error: %s", err) + } + + initCmd, err := tf.initCmd(context.Background()) + + if err != nil { + t.Fatalf("unexpected command error: %s", err) + } + + assertCmd(t, []string{ + "init", + "-no-color", + "-force-copy", + "-input=false", + "-backend=true", + "-get=true", + "-upgrade=false", + }, map[string]string{ + "CLEARENV": "1", + "TF_LOG": "", + "TF_LOG_CORE": "", + "TF_LOG_PATH": tfLogPath, + "TF_LOG_PROVIDER": "TRACE", + }, initCmd) + }) +} + +func TestSetLogProvider(t *testing.T) { + td := t.TempDir() + + tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1)) + + if err != nil { + t.Fatalf("unexpected NewTerraform error: %s", err) + } + + // Required so all testing environment variables are not copied. + err = tf.SetEnv(map[string]string{ + "CLEARENV": "1", + }) + + if err != nil { + t.Fatalf("unexpected SetEnv error: %s", err) + } + + t.Run("case 1: SetLogProvider <= 0.15 error", func(t *testing.T) { + if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { + t.Skip("Terraform for darwin/arm64 is not available until v1") + } + + td012 := t.TempDir() + + tf012, err := NewTerraform(td012, tfVersion(t, testutil.Latest012)) + + if err != nil { + t.Fatalf("unexpected NewTerraform error: %s", err) + } + + err = tf012.SetLogProvider("TRACE") + + if err == nil { + t.Fatal("expected SetLogProvider error, got none") + } + }) + + t.Run("case 2: SetLogProvider TRACE no SetLogPath", func(t *testing.T) { + err := tf.SetLogProvider("TRACE") + + if err != nil { + t.Fatalf("unexpected SetLogProvider error: %s", err) + } + + initCmd, err := tf.initCmd(context.Background()) + + if err != nil { + t.Fatalf("unexpected command error: %s", err) + } + + assertCmd(t, []string{ + "init", + "-no-color", + "-force-copy", + "-input=false", + "-backend=true", + "-get=true", + "-upgrade=false", + }, map[string]string{ + "CLEARENV": "1", + "TF_LOG": "", + "TF_LOG_CORE": "", + "TF_LOG_PATH": "", + "TF_LOG_PROVIDER": "", }, initCmd) }) - t.Run("case 2: SetLogPath sets TF_LOG and TF_LOG_PATH", func(t *testing.T) { + t.Run("case 3: SetLogProvider TRACE and SetLogPath", func(t *testing.T) { tfLogPath := filepath.Join(td, "test.log") + err := tf.SetLogProvider("TRACE") + + if err != nil { + t.Fatalf("unexpected SetLogProvider error: %s", err) + } + + err = tf.SetLogPath(tfLogPath) + + if err != nil { + t.Fatalf("unexpected SetLogPath error: %s", err) + } + + initCmd, err := tf.initCmd(context.Background()) + + if err != nil { + t.Fatalf("unexpected command error: %s", err) + } + + assertCmd(t, []string{ + "init", + "-no-color", + "-force-copy", + "-input=false", + "-backend=true", + "-get=true", + "-upgrade=false", + }, map[string]string{ + "CLEARENV": "1", + "TF_LOG": "", + "TF_LOG_CORE": "", + "TF_LOG_PATH": tfLogPath, + "TF_LOG_PROVIDER": "TRACE", + }, initCmd) + }) + + t.Run("case 4: SetLogProvider DEBUG and SetLogPath", func(t *testing.T) { + tfLogPath := filepath.Join(td, "test.log") + + err := tf.SetLogProvider("DEBUG") + + if err != nil { + t.Fatalf("unexpected SetLogProvider error: %s", err) + } + err = tf.SetLogPath(tfLogPath) if err != nil { @@ -265,9 +664,11 @@ func TestSetLogPath(t *testing.T) { "-get=true", "-upgrade=false", }, map[string]string{ - "CLEARENV": "1", - "TF_LOG": "TRACE", - "TF_LOG_PATH": tfLogPath, + "CLEARENV": "1", + "TF_LOG": "", + "TF_LOG_CORE": "", + "TF_LOG_PATH": tfLogPath, + "TF_LOG_PROVIDER": "DEBUG", }, initCmd) }) }