github.com/mponton/terratest@v0.44.0/modules/test-structure/test_structure.go (about)

     1  package test_structure
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	go_test "testing"
    10  
    11  	"github.com/mponton/terratest/modules/files"
    12  	"github.com/mponton/terratest/modules/logger"
    13  	"github.com/mponton/terratest/modules/opa"
    14  	"github.com/mponton/terratest/modules/terraform"
    15  	"github.com/mponton/terratest/modules/testing"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  // SKIP_STAGE_ENV_VAR_PREFIX is the prefix used for skipping stage environment variables.
    20  const SKIP_STAGE_ENV_VAR_PREFIX = "SKIP_"
    21  
    22  // RunTestStage executes the given test stage (e.g., setup, teardown, validation) if an environment variable of the name
    23  // `SKIP_<stageName>` (e.g., SKIP_teardown) is not set.
    24  func RunTestStage(t testing.TestingT, stageName string, stage func()) {
    25  	envVarName := fmt.Sprintf("%s%s", SKIP_STAGE_ENV_VAR_PREFIX, stageName)
    26  	if os.Getenv(envVarName) == "" {
    27  		logger.Logf(t, "The '%s' environment variable is not set, so executing stage '%s'.", envVarName, stageName)
    28  		stage()
    29  	} else {
    30  		logger.Logf(t, "The '%s' environment variable is set, so skipping stage '%s'.", envVarName, stageName)
    31  	}
    32  }
    33  
    34  // SkipStageEnvVarSet returns true if an environment variable is set instructing Terratest to skip a test stage. This can be an easy way
    35  // to tell if the tests are running in a local dev environment vs a CI server.
    36  func SkipStageEnvVarSet() bool {
    37  	for _, environmentVariable := range os.Environ() {
    38  		if strings.HasPrefix(environmentVariable, SKIP_STAGE_ENV_VAR_PREFIX) {
    39  			return true
    40  		}
    41  	}
    42  
    43  	return false
    44  }
    45  
    46  // CopyTerraformFolderToTemp copies the given root folder to a randomly-named temp folder and return the path to the
    47  // given terraform modules folder within the new temp root folder. This is useful when running multiple tests in
    48  // parallel against the same set of Terraform files to ensure the tests don't overwrite each other's .terraform working
    49  // directory and terraform.tfstate files. To ensure relative paths work, we copy over the entire root folder to a temp
    50  // folder, and then return the path within that temp folder to the given terraform module dir, which is where the actual
    51  // test will be running.
    52  // For example, suppose you had the target terraform folder you want to test in "/examples/terraform-aws-example"
    53  // relative to the repo root. If your tests reside in the "/test" relative to the root, then you will use this as
    54  // follows:
    55  //
    56  //	// Root folder where terraform files should be (relative to the test folder)
    57  //	rootFolder := ".."
    58  //
    59  //	// Relative path to terraform module being tested from the root folder
    60  //	terraformFolderRelativeToRoot := "examples/terraform-aws-example"
    61  //
    62  //	// Copy the terraform folder to a temp folder
    63  //	tempTestFolder := test_structure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot)
    64  //
    65  //	// Make sure to use the temp test folder in the terraform options
    66  //	terraformOptions := &terraform.Options{
    67  //			TerraformDir: tempTestFolder,
    68  //	}
    69  //
    70  // Note that if any of the SKIP_<stage> environment variables is set, we assume this is a test in the local dev where
    71  // there are no other concurrent tests running and we want to be able to cache test data between test stages, so in that
    72  // case, we do NOT copy anything to a temp folder, and return the path to the original terraform module folder instead.
    73  func CopyTerraformFolderToTemp(t testing.TestingT, rootFolder string, terraformModuleFolder string) string {
    74  	return CopyTerraformFolderToDest(t, rootFolder, terraformModuleFolder, os.TempDir())
    75  }
    76  
    77  // CopyTerraformFolderToDest copies the given root folder to a randomly-named temp folder and return the path to the
    78  // given terraform modules folder within the new temp root folder. This is useful when running multiple tests in
    79  // parallel against the same set of Terraform files to ensure the tests don't overwrite each other's .terraform working
    80  // directory and terraform.tfstate files. To ensure relative paths work, we copy over the entire root folder to a temp
    81  // folder, and then return the path within that temp folder to the given terraform module dir, which is where the actual
    82  // test will be running.
    83  // For example, suppose you had the target terraform folder you want to test in "/examples/terraform-aws-example"
    84  // relative to the repo root. If your tests reside in the "/test" relative to the root, then you will use this as
    85  // follows:
    86  //
    87  //	// Destination for the copy of the files.  In this example we are using the Azure Dev Ops variable
    88  //	// for the folder that is cleaned after each pipeline job.
    89  //	destRootFolder := os.Getenv("AGENT_TEMPDIRECTORY")
    90  //
    91  //	// Root folder where terraform files should be (relative to the test folder)
    92  //	rootFolder := ".."
    93  //
    94  //	// Relative path to terraform module being tested from the root folder
    95  //	terraformFolderRelativeToRoot := "examples/terraform-aws-example"
    96  //
    97  //	// Copy the terraform folder to a temp folder
    98  //	tempTestFolder := test_structure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot, destRootFolder)
    99  //
   100  //	// Make sure to use the temp test folder in the terraform options
   101  //	terraformOptions := &terraform.Options{
   102  //			TerraformDir: tempTestFolder,
   103  //	}
   104  //
   105  // Note that if any of the SKIP_<stage> environment variables is set, we assume this is a test in the local dev where
   106  // there are no other concurrent tests running and we want to be able to cache test data between test stages, so in that
   107  // case, we do NOT copy anything to a temp folder, and return the path to the original terraform module folder instead.
   108  func CopyTerraformFolderToDest(t testing.TestingT, rootFolder string, terraformModuleFolder string, destRootFolder string) string {
   109  	if SkipStageEnvVarSet() {
   110  		logger.Logf(t, "A SKIP_XXX environment variable is set. Using original examples folder rather than a temp folder so we can cache data between stages for faster local testing.")
   111  		return filepath.Join(rootFolder, terraformModuleFolder)
   112  	}
   113  
   114  	fullTerraformModuleFolder := filepath.Join(rootFolder, terraformModuleFolder)
   115  
   116  	exists, err := files.FileExistsE(fullTerraformModuleFolder)
   117  	require.NoError(t, err)
   118  	if !exists {
   119  		t.Fatal(files.DirNotFoundError{Directory: fullTerraformModuleFolder})
   120  	}
   121  
   122  	tmpRootFolder, err := files.CopyTerraformFolderToDest(rootFolder, destRootFolder, cleanName(t.Name()))
   123  	if err != nil {
   124  		t.Fatal(err)
   125  	}
   126  
   127  	tmpTestFolder := filepath.Join(tmpRootFolder, terraformModuleFolder)
   128  
   129  	// Log temp folder so we can see it
   130  	logger.Logf(t, "Copied terraform folder %s to %s", fullTerraformModuleFolder, tmpTestFolder)
   131  
   132  	return tmpTestFolder
   133  }
   134  
   135  func cleanName(originalName string) string {
   136  	parts := strings.Split(originalName, "/")
   137  	return parts[len(parts)-1]
   138  }
   139  
   140  // ValidateAllTerraformModules automatically finds all folders specified in RootDir that contain .tf files and runs
   141  // InitAndValidate in all of them.
   142  // Filters down to only those paths passed in ValidationOptions.IncludeDirs, if passed.
   143  // Excludes any folders specified in the ValidationOptions.ExcludeDirs. IncludeDirs will take precedence over ExcludeDirs
   144  // Use the NewValidationOptions method to pass relative paths for either of these options to have the full paths built
   145  // Note that go_test is an alias to Golang's native testing package created to avoid naming conflicts with Terratest's
   146  // own testing package. We are using the native testing.T here because Terratest's testing.T struct does not implement Run
   147  // Note that we have opted to place the ValidateAllTerraformModules function here instead of in the terraform package
   148  // to avoid import cycling
   149  func ValidateAllTerraformModules(t *go_test.T, opts *ValidationOptions) {
   150  	runValidateOnAllTerraformModules(
   151  		t,
   152  		opts,
   153  		func(t *go_test.T, fileType ValidateFileType, tfOpts *terraform.Options) {
   154  			if fileType == TG {
   155  				tfOpts.TerraformBinary = "terragrunt"
   156  				// First call init and terraform validate
   157  				terraform.InitAndValidate(t, tfOpts)
   158  				// Next, call terragrunt validate-inputs which will catch mis-aligned inputs provided via Terragrunt
   159  				terraform.ValidateInputs(t, tfOpts)
   160  			} else if fileType == TF {
   161  				terraform.InitAndValidate(t, tfOpts)
   162  			}
   163  		},
   164  	)
   165  }
   166  
   167  // OPAEvalAllTerraformModules automatically finds all folders specified in RootDir that contain .tf files and runs
   168  // OPAEval in all of them. The behavior of this function is similar to ValidateAllTerraformModules. Refer to the docs of
   169  // that function for more details.
   170  func OPAEvalAllTerraformModules(
   171  	t *go_test.T,
   172  	opts *ValidationOptions,
   173  	opaEvalOpts *opa.EvalOptions,
   174  	resultQuery string,
   175  ) {
   176  	if opts.FileType != TF {
   177  		t.Fatalf("OPAEvalAllTerraformModules currently only works with Terraform modules")
   178  	}
   179  	runValidateOnAllTerraformModules(
   180  		t,
   181  		opts,
   182  		func(t *go_test.T, _ ValidateFileType, tfOpts *terraform.Options) {
   183  			terraform.OPAEval(t, tfOpts, opaEvalOpts, resultQuery)
   184  		},
   185  	)
   186  }
   187  
   188  // runValidateOnAllTerraformModules main driver for ValidateAllTerraformModules and OPAEvalAllTerraformModules. Refer to
   189  // the function docs of ValidateAllTerraformModules for more details.
   190  func runValidateOnAllTerraformModules(
   191  	t *go_test.T,
   192  	opts *ValidationOptions,
   193  	validationFunc func(t *go_test.T, fileType ValidateFileType, tfOps *terraform.Options),
   194  ) {
   195  	dirsToValidate, readErr := FindTerraformModulePathsInRootE(opts)
   196  	require.NoError(t, readErr)
   197  
   198  	for _, dir := range dirsToValidate {
   199  		dir := dir
   200  		t.Run(strings.TrimLeft(dir, "/"), func(t *go_test.T) {
   201  			// Determine the absolute path to the git repository root
   202  			cwd, cwdErr := os.Getwd()
   203  			require.NoError(t, cwdErr)
   204  			gitRoot, gitRootErr := filepath.Abs(filepath.Join(cwd, "../../"))
   205  			require.NoError(t, gitRootErr)
   206  
   207  			// Determine the relative path to the example, module, etc that is currently being considered
   208  			relativePath, pathErr := filepath.Rel(gitRoot, dir)
   209  			require.NoError(t, pathErr)
   210  			// Copy git root to tmp and supply the path to the current module to run init and validate on
   211  			testFolder := CopyTerraformFolderToTemp(t, gitRoot, relativePath)
   212  			require.NotNil(t, testFolder)
   213  
   214  			// Run Terraform init and terraform validate on the test folder that was copied to /tmp
   215  			// to avoid any potential conflicts with tests that may not use the same copy to /tmp behavior
   216  			tfOpts := &terraform.Options{TerraformDir: testFolder}
   217  			validationFunc(t, opts.FileType, tfOpts)
   218  		})
   219  	}
   220  }