1+ // Package opa provides helpers for running Open Policy Agent (OPA) evaluations in automated tests.
12package opa
23
34import (
5+ "context"
6+ "fmt"
47 "path/filepath"
58 "strings"
69 "sync"
@@ -15,9 +18,6 @@ import (
1518// EvalOptions defines options that can be passed to the 'opa eval' command for checking policies on arbitrary JSON data
1619// via OPA.
1720type EvalOptions struct {
18- // Whether OPA should run checks with failure.
19- FailMode FailMode
20-
2121 // Path to rego file containing the OPA rules. Can also be a remote path defined in go-getter syntax. Refer to
2222 // https://github.com/hashicorp/go-getter#url-format for supported options.
2323 RulePath string
@@ -31,6 +31,9 @@ type EvalOptions struct {
3131 // Example: []string{"--strict"} to enable strict mode for the eval subcommand.
3232 ExtraArgs []string
3333
34+ // Whether OPA should run checks with failure.
35+ FailMode FailMode
36+
3437 // The following options can be used to change the behavior of the related functions for debuggability.
3538
3639 // When true, keep any temp files and folders that are created for the purpose of running opa eval.
@@ -52,7 +55,7 @@ const (
5255 NoFail
5356)
5457
55- // EvalE runs `opa eval` on the given JSON files using the configured policy file and result query. Translates to:
58+ // Eval runs `opa eval` on the given JSON files using the configured policy file and result query. Translates to:
5659//
5760// opa eval -i $JSONFile -d $RulePath $ResultQuery
5861//
@@ -62,7 +65,8 @@ func Eval(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resu
6265 require .NoError (t , EvalE (t , options , jsonFilePaths , resultQuery ))
6366}
6467
65- // EvalE runs `opa eval` on the given JSON files using the configured policy file and result query. Translates to:
68+ // EvalWithOutput runs `opa eval` on the given JSON files using the configured policy file and result query.
69+ // Translates to:
6670//
6771// opa eval -i $JSONFile -d $RulePath $ResultQuery
6872//
@@ -72,6 +76,7 @@ func Eval(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resu
7276func EvalWithOutput (t testing.TestingT , options * EvalOptions , jsonFilePaths []string , resultQuery string ) (outputs []string ) {
7377 outputs , err := EvalWithOutputE (t , options , jsonFilePaths , resultQuery )
7478 require .NoError (t , err )
79+
7580 return
7681}
7782
@@ -82,6 +87,7 @@ func EvalWithOutput(t testing.TestingT, options *EvalOptions, jsonFilePaths []st
8287// This will asynchronously run OPA on each file concurrently using goroutines.
8388func EvalE (t testing.TestingT , options * EvalOptions , jsonFilePaths []string , resultQuery string ) (err error ) {
8489 _ , err = evalE (t , options , jsonFilePaths , resultQuery )
90+
8591 return
8692}
8793
@@ -98,14 +104,17 @@ func EvalWithOutputE(t testing.TestingT, options *EvalOptions, jsonFilePaths []s
98104func evalE (t testing.TestingT , options * EvalOptions , jsonFilePaths []string , resultQuery string ) (outputs []string , err error ) {
99105 downloadedPolicyPath , err := DownloadPolicyE (t , options .RulePath )
100106 if err != nil {
101- return
107+ return nil , fmt . Errorf ( "downloading policy %s: %w" , options . RulePath , err )
102108 }
103109
104110 outputs = make ([]string , len (jsonFilePaths ))
105111 wg := new (sync.WaitGroup )
106112 wg .Add (len (jsonFilePaths ))
113+
107114 errorsOccurred := new (multierror.Error )
115+
108116 errChans := make ([]chan error , len (jsonFilePaths ))
117+
109118 for i , jsonFilePath := range jsonFilePaths {
110119 errChan := make (chan error , 1 )
111120 errChans [i ] = errChan
@@ -114,13 +123,16 @@ func evalE(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, res
114123 outputs [i ] = asyncEval (t , wg , errChan , options , downloadedPolicyPath , jsonFilePath , resultQuery )
115124 }(i , jsonFilePath )
116125 }
126+
117127 wg .Wait ()
128+
118129 for _ , errChan := range errChans {
119130 err := <- errChan
120131 if err != nil {
121132 errorsOccurred = multierror .Append (errorsOccurred , err )
122133 }
123134 }
135+
124136 return outputs , errorsOccurred .ErrorOrNil ()
125137}
126138
@@ -135,27 +147,33 @@ func asyncEval(
135147 resultQuery string ,
136148) (output string ) {
137149 defer wg .Done ()
138- cmd := shell.Command {
150+
151+ cmd := & shell.Command {
139152 Command : "opa" ,
140153 Args : formatOPAEvalArgs (options , downloadedPolicyPath , jsonFilePath , resultQuery ),
141154
142155 // Do not log output from shell package so we can log the full json without breaking it up. This is ok, because
143156 // opa eval is typically very quick.
144157 Logger : logger .Discard ,
145158 }
159+
146160 output , err := runCommandWithFullLoggingE (t , options .Logger , cmd )
161+
147162 ruleBasePath := filepath .Base (downloadedPolicyPath )
163+
148164 if err == nil {
149165 options .Logger .Logf (t , "opa eval passed on file %s (policy %s; query %s)" , jsonFilePath , ruleBasePath , resultQuery )
150166 } else {
151167 options .Logger .Logf (t , "Failed opa eval on file %s (policy %s; query %s)" , jsonFilePath , ruleBasePath , resultQuery )
152- if options .DebugDisableQueryDataOnError == false {
168+
169+ if ! options .DebugDisableQueryDataOnError {
153170 options .Logger .Logf (t , "DEBUG: rerunning opa eval to query for full data." )
154171 cmd .Args = formatOPAEvalArgs (options , downloadedPolicyPath , jsonFilePath , "data" )
155172 // We deliberately ignore the error here as we want to only return the original error.
156173 output , _ = runCommandWithFullLoggingE (t , options .Logger , cmd )
157174 }
158175 }
176+
159177 errChan <- err
160178
161179 return
@@ -179,6 +197,8 @@ func formatOPAEvalArgs(options *EvalOptions, rulePath, jsonFilePath, resultQuery
179197 args = append (args , "--fail" )
180198 case FailDefined :
181199 args = append (args , "--fail-defined" )
200+ case NoFail :
201+ // No additional flags needed.
182202 }
183203
184204 args = append (
@@ -189,14 +209,16 @@ func formatOPAEvalArgs(options *EvalOptions, rulePath, jsonFilePath, resultQuery
189209 resultQuery ,
190210 }... ,
191211 )
212+
192213 return args
193214}
194215
195- // runCommandWithFullLogging will log the command output in its entirety with buffering. This avoids breaking up the
216+ // runCommandWithFullLoggingE will log the command output in its entirety with buffering. This avoids breaking up the
196217// logs when commands are run concurrently. This is a private function used in the context of opa only because opa runs
197218// very quickly, and the output of opa is hard to parse if it is broken up by interleaved logs.
198- func runCommandWithFullLoggingE (t testing.TestingT , logger * logger.Logger , cmd shell.Command ) (output string , err error ) {
199- output , err = shell .RunCommandAndGetOutputE (t , cmd )
200- logger .Logf (t , "Output of command `%s %s`:\n %s" , cmd .Command , strings .Join (cmd .Args , " " ), output )
219+ func runCommandWithFullLoggingE (t testing.TestingT , lgr * logger.Logger , cmd * shell.Command ) (output string , err error ) {
220+ output , err = shell .RunCommandContextAndGetOutputE (t , context .Background (), cmd )
221+ lgr .Logf (t , "Output of command `%s %s`:\n %s" , cmd .Command , strings .Join (cmd .Args , " " ), output )
222+
201223 return
202224}
0 commit comments