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 }