github.com/someshkoli/terratest@v0.41.1/modules/opa/eval.go (about)

     1  package opa
     2  
     3  import (
     4  	"path/filepath"
     5  	"strings"
     6  	"sync"
     7  
     8  	"github.com/gruntwork-io/terratest/modules/logger"
     9  	"github.com/gruntwork-io/terratest/modules/shell"
    10  	"github.com/gruntwork-io/terratest/modules/testing"
    11  	"github.com/hashicorp/go-multierror"
    12  	"github.com/stretchr/testify/require"
    13  )
    14  
    15  // EvalOptions defines options that can be passed to the 'opa eval' command for checking policies on arbitrary JSON data
    16  // via OPA.
    17  type EvalOptions struct {
    18  	// Whether OPA should run checks with failure.
    19  	FailMode FailMode
    20  
    21  	// Path to rego file containing the OPA rules. Can also be a remote path defined in go-getter syntax. Refer to
    22  	// https://github.com/hashicorp/go-getter#url-format for supported options.
    23  	RulePath string
    24  
    25  	// Set a logger that should be used. See the logger package for more info.
    26  	Logger *logger.Logger
    27  
    28  	// The following options can be used to change the behavior of the related functions for debuggability.
    29  
    30  	// When true, keep any temp files and folders that are created for the purpose of running opa eval.
    31  	DebugKeepTempFiles bool
    32  
    33  	// When true, disable the functionality where terratest reruns the opa check on the same file and query all elements
    34  	// on error. By default, terratest will rerun the opa eval call with `data` query so you can see all the contents
    35  	// evaluated.
    36  	DebugDisableQueryDataOnError bool
    37  }
    38  
    39  // FailMode signals whether `opa eval` should fail when the query returns an undefined value (FailUndefined), a
    40  // defined value (FailDefined), or not at all (NoFail).
    41  type FailMode int
    42  
    43  const (
    44  	FailUndefined FailMode = iota
    45  	FailDefined
    46  	NoFail
    47  )
    48  
    49  // EvalE runs `opa eval` on the given JSON files using the configured policy file and result query. Translates to:
    50  //     opa eval -i $JSONFile -d $RulePath $ResultQuery
    51  // This will asynchronously run OPA on each file concurrently using goroutines.
    52  // This will fail the test if any one of the files failed.
    53  func Eval(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) {
    54  	require.NoError(t, EvalE(t, options, jsonFilePaths, resultQuery))
    55  }
    56  
    57  // EvalE runs `opa eval` on the given JSON files using the configured policy file and result query. Translates to:
    58  //     opa eval -i $JSONFile -d $RulePath $ResultQuery
    59  // This will asynchronously run OPA on each file concurrently using goroutines.
    60  func EvalE(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) error {
    61  	downloadedPolicyPath, err := DownloadPolicyE(t, options.RulePath)
    62  	if err != nil {
    63  		return err
    64  	}
    65  
    66  	wg := new(sync.WaitGroup)
    67  	wg.Add(len(jsonFilePaths))
    68  	errorsOccurred := new(multierror.Error)
    69  	errChans := make([]chan error, len(jsonFilePaths))
    70  	for i, jsonFilePath := range jsonFilePaths {
    71  		errChan := make(chan error, 1)
    72  		errChans[i] = errChan
    73  		go asyncEval(t, wg, errChan, options, downloadedPolicyPath, jsonFilePath, resultQuery)
    74  	}
    75  	wg.Wait()
    76  	for _, errChan := range errChans {
    77  		err := <-errChan
    78  		if err != nil {
    79  			errorsOccurred = multierror.Append(errorsOccurred, err)
    80  		}
    81  	}
    82  	return errorsOccurred.ErrorOrNil()
    83  }
    84  
    85  // asyncEval is a function designed to be run in a goroutine to asynchronously call `opa eval` on a single input file.
    86  func asyncEval(
    87  	t testing.TestingT,
    88  	wg *sync.WaitGroup,
    89  	errChan chan error,
    90  	options *EvalOptions,
    91  	downloadedPolicyPath string,
    92  	jsonFilePath string,
    93  	resultQuery string,
    94  ) {
    95  	defer wg.Done()
    96  	cmd := shell.Command{
    97  		Command: "opa",
    98  		Args:    formatOPAEvalArgs(options, downloadedPolicyPath, jsonFilePath, resultQuery),
    99  
   100  		// Do not log output from shell package so we can log the full json without breaking it up. This is ok, because
   101  		// opa eval is typically very quick.
   102  		Logger: logger.Discard,
   103  	}
   104  	err := runCommandWithFullLoggingE(t, options.Logger, cmd)
   105  	ruleBasePath := filepath.Base(downloadedPolicyPath)
   106  	if err == nil {
   107  		options.Logger.Logf(t, "opa eval passed on file %s (policy %s; query %s)", jsonFilePath, ruleBasePath, resultQuery)
   108  	} else {
   109  		options.Logger.Logf(t, "Failed opa eval on file %s (policy %s; query %s)", jsonFilePath, ruleBasePath, resultQuery)
   110  		if options.DebugDisableQueryDataOnError == false {
   111  			options.Logger.Logf(t, "DEBUG: rerunning opa eval to query for full data.")
   112  			cmd.Args = formatOPAEvalArgs(options, downloadedPolicyPath, jsonFilePath, "data")
   113  			// We deliberately ignore the error here as we want to only return the original error.
   114  			runCommandWithFullLoggingE(t, options.Logger, cmd)
   115  		}
   116  	}
   117  	errChan <- err
   118  }
   119  
   120  // formatOPAEvalArgs formats the arguments for the `opa eval` command.
   121  func formatOPAEvalArgs(options *EvalOptions, rulePath, jsonFilePath, resultQuery string) []string {
   122  	args := []string{"eval"}
   123  
   124  	switch options.FailMode {
   125  	case FailUndefined:
   126  		args = append(args, "--fail")
   127  	case FailDefined:
   128  		args = append(args, "--fail-defined")
   129  	}
   130  
   131  	args = append(
   132  		args,
   133  		[]string{
   134  			"-i", jsonFilePath,
   135  			"-d", rulePath,
   136  			resultQuery,
   137  		}...,
   138  	)
   139  	return args
   140  }
   141  
   142  // runCommandWithFullLogging will log the command output in its entirety with buffering. This avoids breaking up the
   143  // logs when commands are run concurrently. This is a private function used in the context of opa only because opa runs
   144  // very quickly, and the output of opa is hard to parse if it is broken up by interleaved logs.
   145  func runCommandWithFullLoggingE(t testing.TestingT, logger *logger.Logger, cmd shell.Command) error {
   146  	output, err := shell.RunCommandAndGetOutputE(t, cmd)
   147  	logger.Logf(t, "Output of command `%s %s`:\n%s", cmd.Command, strings.Join(cmd.Args, " "), output)
   148  	return err
   149  }