2022-08-06 14:21:18 +00:00
package plugintest
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/hashicorp/terraform-exec/tfexec"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging"
)
// AutoInitProviderHelper is the main entrypoint for testing provider plugins
// using this package. It is intended to be called during TestMain to prepare
// for provider testing.
//
// AutoInitProviderHelper will discover the location of a current Terraform CLI
// executable to test against, detect whether a prior version of the plugin is
// available for upgrade tests, and then will return an object containing the
// results of that initialization which can then be stored in a global variable
// for use in other tests.
func AutoInitProviderHelper ( ctx context . Context , sourceDir string ) * Helper {
helper , err := AutoInitHelper ( ctx , sourceDir )
if err != nil {
fmt . Fprintf ( os . Stderr , "cannot run Terraform provider tests: %s\n" , err )
os . Exit ( 1 )
}
return helper
}
// Helper is intended as a per-package singleton created in TestMain which
// other tests in a package can use to create Terraform execution contexts
type Helper struct {
baseDir string
// sourceDir is the dir containing the provider source code, needed
// for tests that use fixture files.
sourceDir string
terraformExec string
// execTempDir is created during DiscoverConfig to store any downloaded
// binaries
execTempDir string
}
// AutoInitHelper uses the auto-discovery behavior of DiscoverConfig to prepare
// a configuration and then calls InitHelper with it. This is a convenient
// way to get the standard init behavior based on environment variables, and
// callers should use this unless they have an unusual requirement that calls
// for constructing a config in a different way.
func AutoInitHelper ( ctx context . Context , sourceDir string ) ( * Helper , error ) {
config , err := DiscoverConfig ( ctx , sourceDir )
if err != nil {
return nil , err
}
return InitHelper ( ctx , config )
}
// InitHelper prepares a testing helper with the given configuration.
//
// For most callers it is sufficient to call AutoInitHelper instead, which
// will construct a configuration automatically based on certain environment
// variables.
//
// If this function returns an error then it may have left some temporary files
// behind in the system's temporary directory. There is currently no way to
// automatically clean those up.
func InitHelper ( ctx context . Context , config * Config ) ( * Helper , error ) {
tempDir := os . Getenv ( EnvTfAccTempDir )
2022-12-24 16:57:19 +00:00
baseDir , err := os . MkdirTemp ( tempDir , "plugintest" )
2022-08-06 14:21:18 +00:00
if err != nil {
return nil , fmt . Errorf ( "failed to create temporary directory for test helper: %s" , err )
}
return & Helper {
baseDir : baseDir ,
sourceDir : config . SourceDir ,
terraformExec : config . TerraformExec ,
execTempDir : config . execTempDir ,
} , nil
}
// Close cleans up temporary files and directories created to support this
// helper, returning an error if any of the cleanup fails.
//
// Call this before returning from TestMain to minimize the amount of detritus
// left behind in the filesystem after the tests complete.
func ( h * Helper ) Close ( ) error {
if h . execTempDir != "" {
err := os . RemoveAll ( h . execTempDir )
if err != nil {
return err
}
}
return os . RemoveAll ( h . baseDir )
}
// NewWorkingDir creates a new working directory for use in the implementation
// of a single test, returning a WorkingDir object representing that directory.
//
// If the working directory object is not itself closed by the time the test
// program exits, the Close method on the helper itself will attempt to
// delete it.
func ( h * Helper ) NewWorkingDir ( ctx context . Context , t TestControl ) ( * WorkingDir , error ) {
2022-12-24 16:57:19 +00:00
dir , err := os . MkdirTemp ( h . baseDir , "work" )
2022-08-06 14:21:18 +00:00
if err != nil {
return nil , err
}
ctx = logging . TestWorkingDirectoryContext ( ctx , dir )
// symlink the provider source files into the config directory
// e.g. testdata
logging . HelperResourceTrace ( ctx , "Symlinking source directories to work directory" )
err = symlinkDirectoriesOnly ( h . sourceDir , dir )
if err != nil {
return nil , err
}
tf , err := tfexec . NewTerraform ( dir , h . terraformExec )
if err != nil {
return nil , fmt . Errorf ( "unable to create terraform-exec instance: %w" , err )
}
err = tf . SetDisablePluginTLS ( true )
if err != nil {
return nil , fmt . Errorf ( "unable to disable terraform-exec plugin TLS: %w" , err )
}
err = tf . SetSkipProviderVerify ( true ) // Only required for Terraform CLI 0.12.x
var mismatch * tfexec . ErrVersionMismatch
if err != nil && ! errors . As ( err , & mismatch ) {
return nil , fmt . Errorf ( "unable to disable terraform-exec provider verification: %w" , err )
}
tfAccLog := os . Getenv ( EnvTfAccLog )
tfAccLogPath := os . Getenv ( EnvTfAccLogPath )
tfLogCore := os . Getenv ( EnvTfLogCore )
tfLogPathMask := os . Getenv ( EnvTfLogPathMask )
tfLogProvider := os . Getenv ( EnvTfLogProvider )
if tfAccLog != "" && tfLogCore != "" {
err = fmt . Errorf (
"Invalid environment variable configuration. Cannot set both TF_ACC_LOG and TF_LOG_CORE. " +
"Use TF_LOG_CORE and TF_LOG_PROVIDER to separately control the Terraform CLI logging subsystems. " +
"To control the Go standard library log package for the provider under test, use TF_LOG." ,
)
logging . HelperResourceError ( ctx , err . Error ( ) )
return nil , err
}
if tfAccLog != "" {
logging . HelperResourceTrace (
ctx ,
fmt . Sprintf ( "Setting terraform-exec log level via %s environment variable, if Terraform CLI is version 0.15 or later" , EnvTfAccLog ) ,
map [ string ] interface { } { logging . KeyTestTerraformLogLevel : tfAccLog } ,
)
err := tf . SetLog ( tfAccLog )
if err != nil {
if ! errors . As ( err , new ( * tfexec . ErrVersionMismatch ) ) {
logging . HelperResourceError (
ctx ,
"Unable to set terraform-exec log level" ,
map [ string ] interface { } { logging . KeyError : err . Error ( ) } ,
)
return nil , fmt . Errorf ( "unable to set terraform-exec log level (%s): %w" , tfAccLog , err )
}
logging . HelperResourceWarn (
ctx ,
fmt . Sprintf ( "Unable to set terraform-exec log level via %s environment variable, as Terraform CLI is version 0.14 or earlier. It will default to TRACE." , EnvTfAccLog ) ,
map [ string ] interface { } { logging . KeyTestTerraformLogLevel : "TRACE" } ,
)
}
}
if tfLogCore != "" {
logging . HelperResourceTrace (
ctx ,
fmt . Sprintf ( "Setting terraform-exec core log level via %s environment variable, if Terraform CLI is version 0.15 or later" , EnvTfLogCore ) ,
map [ string ] interface { } {
logging . KeyTestTerraformLogCoreLevel : tfLogCore ,
} ,
)
err := tf . SetLogCore ( tfLogCore )
if err != nil {
logging . HelperResourceError (
ctx ,
"Unable to set terraform-exec core log level" ,
map [ string ] interface { } { logging . KeyError : err . Error ( ) } ,
)
return nil , fmt . Errorf ( "unable to set terraform-exec core log level (%s): %w" , tfLogCore , err )
}
}
if tfLogProvider != "" {
logging . HelperResourceTrace (
ctx ,
fmt . Sprintf ( "Setting terraform-exec provider log level via %s environment variable, if Terraform CLI is version 0.15 or later" , EnvTfLogProvider ) ,
map [ string ] interface { } {
logging . KeyTestTerraformLogCoreLevel : tfLogProvider ,
} ,
)
err := tf . SetLogProvider ( tfLogProvider )
if err != nil {
logging . HelperResourceError (
ctx ,
"Unable to set terraform-exec provider log level" ,
map [ string ] interface { } { logging . KeyError : err . Error ( ) } ,
)
return nil , fmt . Errorf ( "unable to set terraform-exec provider log level (%s): %w" , tfLogProvider , err )
}
}
var logPath , logPathEnvVar string
if tfAccLogPath != "" {
logPath = tfAccLogPath
logPathEnvVar = EnvTfAccLogPath
}
// Similar to helper/logging.LogOutput() and
// terraform-plugin-log/tfsdklog.RegisterTestSink(), the TF_LOG_PATH_MASK
// environment variable should take precedence over TF_ACC_LOG_PATH.
if tfLogPathMask != "" {
// Escape special characters which may appear if we have subtests
testName := strings . Replace ( t . Name ( ) , "/" , "__" , - 1 )
logPath = fmt . Sprintf ( tfLogPathMask , testName )
logPathEnvVar = EnvTfLogPathMask
}
if logPath != "" {
logging . HelperResourceTrace (
ctx ,
fmt . Sprintf ( "Setting terraform-exec log path via %s environment variable" , logPathEnvVar ) ,
map [ string ] interface { } { logging . KeyTestTerraformLogPath : logPath } ,
)
if err := tf . SetLogPath ( logPath ) ; err != nil {
return nil , fmt . Errorf ( "unable to set terraform-exec log path (%s): %w" , logPath , err )
}
}
return & WorkingDir {
h : h ,
tf : tf ,
baseDir : dir ,
terraformExec : h . terraformExec ,
} , nil
}
// RequireNewWorkingDir is a variant of NewWorkingDir that takes a TestControl
// object and will immediately fail the running test if the creation of the
// working directory fails.
func ( h * Helper ) RequireNewWorkingDir ( ctx context . Context , t TestControl ) * WorkingDir {
t . Helper ( )
wd , err := h . NewWorkingDir ( ctx , t )
if err != nil {
t := testingT { t }
t . Fatalf ( "failed to create new working directory: %s" , err )
return nil
}
return wd
}
// WorkingDirectory returns the working directory being used when running tests.
func ( h * Helper ) WorkingDirectory ( ) string {
return h . baseDir
}
// TerraformExecPath returns the location of the Terraform CLI executable that
// should be used when running tests.
func ( h * Helper ) TerraformExecPath ( ) string {
return h . terraformExec
}