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 }