github.com/mponton/terratest@v0.44.0/modules/opa/eval.go (about)

     1  package opa
     2  
     3  import (
     4  	"path/filepath"
     5  	"strings"
     6  	"sync"
     7  
     8  	"github.com/hashicorp/go-multierror"
     9  	"github.com/mponton/terratest/modules/logger"
    10  	"github.com/mponton/terratest/modules/shell"
    11  	"github.com/mponton/terratest/modules/testing"
    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  //
    51  //	opa eval -i $JSONFile -d $RulePath $ResultQuery
    52  //
    53  // This will asynchronously run OPA on each file concurrently using goroutines.
    54  // This will fail the test if any one of the files failed.
    55  func Eval(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) {
    56  	require.NoError(t, EvalE(t, options, jsonFilePaths, resultQuery))
    57  }
    58  
    59  // EvalE runs `opa eval` on the given JSON files using the configured policy file and result query. Translates to:
    60  //
    61  //	opa eval -i $JSONFile -d $RulePath $ResultQuery
    62  //
    63  // This will asynchronously run OPA on each file concurrently using goroutines.
    64  func EvalE(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) error {
    65  	downloadedPolicyPath, err := DownloadPolicyE(t, options.RulePath)
    66  	if err != nil {
    67  		return err
    68  	}
    69  
    70  	wg := new(sync.WaitGroup)
    71  	wg.Add(len(jsonFilePaths))
    72  	errorsOccurred := new(multierror.Error)
    73  	errChans := make([]chan error, len(jsonFilePaths))
    74  	for i, jsonFilePath := range jsonFilePaths {
    75  		errChan := make(chan error, 1)
    76  		errChans[i] = errChan
    77  		go asyncEval(t, wg, errChan, options, downloadedPolicyPath, jsonFilePath, resultQuery)
    78  	}
    79  	wg.Wait()
    80  	for _, errChan := range errChans {
    81  		err := <-errChan
    82  		if err != nil {
    83  			errorsOccurred = multierror.Append(errorsOccurred, err)
    84  		}
    85  	}
    86  	return errorsOccurred.ErrorOrNil()
    87  }
    88  
    89  // asyncEval is a function designed to be run in a goroutine to asynchronously call `opa eval` on a single input file.
    90  func asyncEval(
    91  	t testing.TestingT,
    92  	wg *sync.WaitGroup,
    93  	errChan chan error,
    94  	options *EvalOptions,
    95  	downloadedPolicyPath string,
    96  	jsonFilePath string,
    97  	resultQuery string,
    98  ) {
    99  	defer wg.Done()
   100  	cmd := shell.Command{
   101  		Command: "opa",
   102  		Args:    formatOPAEvalArgs(options, downloadedPolicyPath, jsonFilePath, resultQuery),
   103  
   104  		// Do not log output from shell package so we can log the full json without breaking it up. This is ok, because
   105  		// opa eval is typically very quick.
   106  		Logger: logger.Discard,
   107  	}
   108  	err := runCommandWithFullLoggingE(t, options.Logger, cmd)
   109  	ruleBasePath := filepath.Base(downloadedPolicyPath)
   110  	if err == nil {
   111  		options.Logger.Logf(t, "opa eval passed on file %s (policy %s; query %s)", jsonFilePath, ruleBasePath, resultQuery)
   112  	} else {
   113  		options.Logger.Logf(t, "Failed opa eval on file %s (policy %s; query %s)", jsonFilePath, ruleBasePath, resultQuery)
   114  		if options.DebugDisableQueryDataOnError == false {
   115  			options.Logger.Logf(t, "DEBUG: rerunning opa eval to query for full data.")
   116  			cmd.Args = formatOPAEvalArgs(options, downloadedPolicyPath, jsonFilePath, "data")
   117  			// We deliberately ignore the error here as we want to only return the original error.
   118  			runCommandWithFullLoggingE(t, options.Logger, cmd)
   119  		}
   120  	}
   121  	errChan <- err
   122  }
   123  
   124  // formatOPAEvalArgs formats the arguments for the `opa eval` command.
   125  func formatOPAEvalArgs(options *EvalOptions, rulePath, jsonFilePath, resultQuery string) []string {
   126  	args := []string{"eval"}
   127  
   128  	switch options.FailMode {
   129  	case FailUndefined:
   130  		args = append(args, "--fail")
   131  	case FailDefined:
   132  		args = append(args, "--fail-defined")
   133  	}
   134  
   135  	args = append(
   136  		args,
   137  		[]string{
   138  			"-i", jsonFilePath,
   139  			"-d", rulePath,
   140  			resultQuery,
   141  		}...,
   142  	)
   143  	return args
   144  }
   145  
   146  // runCommandWithFullLogging will log the command output in its entirety with buffering. This avoids breaking up the
   147  // logs when commands are run concurrently. This is a private function used in the context of opa only because opa runs
   148  // very quickly, and the output of opa is hard to parse if it is broken up by interleaved logs.
   149  func runCommandWithFullLoggingE(t testing.TestingT, logger *logger.Logger, cmd shell.Command) error {
   150  	output, err := shell.RunCommandAndGetOutputE(t, cmd)
   151  	logger.Logf(t, "Output of command `%s %s`:\n%s", cmd.Command, strings.Join(cmd.Args, " "), output)
   152  	return err
   153  }