Skip to content

Commit d54ee08

Browse files
Merge pull request #1468 from gruntwork-io/ova-eval-withoutput
Opa EvalWithOutput
2 parents 4f95711 + 2a6af68 commit d54ee08

File tree

2 files changed

+199
-10
lines changed

2 files changed

+199
-10
lines changed

modules/opa/eval.go

+44-10
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,52 @@ func Eval(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resu
6161
// opa eval -i $JSONFile -d $RulePath $ResultQuery
6262
//
6363
// This will asynchronously run OPA on each file concurrently using goroutines.
64-
func EvalE(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) error {
64+
// This will fail the test if any one of the files failed.
65+
// For each file, the output will be returned on the outputs slice.
66+
func EvalWithOutput(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) (outputs []string) {
67+
outputs, err := EvalWithOutputE(t, options, jsonFilePaths, resultQuery)
68+
require.NoError(t, err)
69+
return
70+
}
71+
72+
// EvalE runs `opa eval` on the given JSON files using the configured policy file and result query. Translates to:
73+
//
74+
// opa eval -i $JSONFile -d $RulePath $ResultQuery
75+
//
76+
// This will asynchronously run OPA on each file concurrently using goroutines.
77+
func EvalE(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) (err error) {
78+
_, err = evalE(t, options, jsonFilePaths, resultQuery)
79+
return
80+
}
81+
82+
// EvalWithOutputE runs `opa eval` on the given JSON files using the configured policy file and result query. Translates to:
83+
//
84+
// opa eval -i $JSONFile -d $RulePath $ResultQuery
85+
//
86+
// This will asynchronously run OPA on each file concurrently using goroutines.
87+
// For each file, the output will be returned on the outputs slice.
88+
func EvalWithOutputE(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) (outputs []string, err error) {
89+
return evalE(t, options, jsonFilePaths, resultQuery)
90+
}
91+
92+
func evalE(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) (outputs []string, err error) {
6593
downloadedPolicyPath, err := DownloadPolicyE(t, options.RulePath)
6694
if err != nil {
67-
return err
95+
return
6896
}
6997

98+
outputs = make([]string, len(jsonFilePaths))
7099
wg := new(sync.WaitGroup)
71100
wg.Add(len(jsonFilePaths))
72101
errorsOccurred := new(multierror.Error)
73102
errChans := make([]chan error, len(jsonFilePaths))
74103
for i, jsonFilePath := range jsonFilePaths {
75104
errChan := make(chan error, 1)
76105
errChans[i] = errChan
77-
go asyncEval(t, wg, errChan, options, downloadedPolicyPath, jsonFilePath, resultQuery)
106+
107+
go func(i int, jsonFilePath string) {
108+
outputs[i] = asyncEval(t, wg, errChan, options, downloadedPolicyPath, jsonFilePath, resultQuery)
109+
}(i, jsonFilePath)
78110
}
79111
wg.Wait()
80112
for _, errChan := range errChans {
@@ -83,7 +115,7 @@ func EvalE(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, res
83115
errorsOccurred = multierror.Append(errorsOccurred, err)
84116
}
85117
}
86-
return errorsOccurred.ErrorOrNil()
118+
return outputs, errorsOccurred.ErrorOrNil()
87119
}
88120

89121
// asyncEval is a function designed to be run in a goroutine to asynchronously call `opa eval` on a single input file.
@@ -95,7 +127,7 @@ func asyncEval(
95127
downloadedPolicyPath string,
96128
jsonFilePath string,
97129
resultQuery string,
98-
) {
130+
) (output string) {
99131
defer wg.Done()
100132
cmd := shell.Command{
101133
Command: "opa",
@@ -105,7 +137,7 @@ func asyncEval(
105137
// opa eval is typically very quick.
106138
Logger: logger.Discard,
107139
}
108-
err := runCommandWithFullLoggingE(t, options.Logger, cmd)
140+
output, err := runCommandWithFullLoggingE(t, options.Logger, cmd)
109141
ruleBasePath := filepath.Base(downloadedPolicyPath)
110142
if err == nil {
111143
options.Logger.Logf(t, "opa eval passed on file %s (policy %s; query %s)", jsonFilePath, ruleBasePath, resultQuery)
@@ -115,10 +147,12 @@ func asyncEval(
115147
options.Logger.Logf(t, "DEBUG: rerunning opa eval to query for full data.")
116148
cmd.Args = formatOPAEvalArgs(options, downloadedPolicyPath, jsonFilePath, "data")
117149
// We deliberately ignore the error here as we want to only return the original error.
118-
runCommandWithFullLoggingE(t, options.Logger, cmd)
150+
output, _ = runCommandWithFullLoggingE(t, options.Logger, cmd)
119151
}
120152
}
121153
errChan <- err
154+
155+
return
122156
}
123157

124158
// formatOPAEvalArgs formats the arguments for the `opa eval` command.
@@ -146,8 +180,8 @@ func formatOPAEvalArgs(options *EvalOptions, rulePath, jsonFilePath, resultQuery
146180
// runCommandWithFullLogging will log the command output in its entirety with buffering. This avoids breaking up the
147181
// logs when commands are run concurrently. This is a private function used in the context of opa only because opa runs
148182
// 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)
183+
func runCommandWithFullLoggingE(t testing.TestingT, logger *logger.Logger, cmd shell.Command) (output string, err error) {
184+
output, err = shell.RunCommandAndGetOutputE(t, cmd)
151185
logger.Logf(t, "Output of command `%s %s`:\n%s", cmd.Command, strings.Join(cmd.Args, " "), output)
152-
return err
186+
return
153187
}

modules/opa/eval_test.go

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package opa
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestEvalWithOutput(t *testing.T) {
12+
t.Parallel()
13+
14+
tests := []struct {
15+
name string
16+
17+
policy string
18+
query string
19+
inputs []string
20+
outputs []string
21+
isError bool
22+
}{
23+
{
24+
name: "Success",
25+
policy: `
26+
package test
27+
allow {
28+
startswith(input.user, "admin")
29+
}
30+
`,
31+
query: "data.test.allow",
32+
inputs: []string{
33+
`{"user": "admin-1"}`,
34+
`{"user": "admin-2"}`,
35+
`{"user": "admin-3"}`,
36+
},
37+
outputs: []string{
38+
`{
39+
"result": [{
40+
"expressions": [{
41+
"value": true,
42+
"text": "data.test.allow",
43+
"location": {
44+
"row": 1,
45+
"col": 1
46+
}
47+
}]
48+
}]
49+
}`,
50+
`{
51+
"result": [{
52+
"expressions": [{
53+
"value": true,
54+
"text": "data.test.allow",
55+
"location": {
56+
"row": 1,
57+
"col": 1
58+
}
59+
}]
60+
}]
61+
}`,
62+
`{
63+
"result": [{
64+
"expressions": [{
65+
"value": true,
66+
"text": "data.test.allow",
67+
"location": {
68+
"row": 1,
69+
"col": 1
70+
}
71+
}]
72+
}]
73+
}`,
74+
},
75+
},
76+
{
77+
name: "ContainsError",
78+
policy: `
79+
package test
80+
allow {
81+
input.user == "admin"
82+
}
83+
`,
84+
query: "data.test.allow",
85+
isError: true,
86+
inputs: []string{
87+
`{"user": "admin"}`,
88+
`{"user": "nobody"}`,
89+
},
90+
outputs: []string{
91+
`{
92+
"result": [{
93+
"expressions": [{
94+
"value": true,
95+
"text": "data.test.allow",
96+
"location": {
97+
"row": 1,
98+
"col": 1
99+
}
100+
}]
101+
}]
102+
}`,
103+
`{
104+
"result": [{
105+
"expressions": [{
106+
"value": {
107+
"test": {}
108+
},
109+
"text": "data",
110+
"location": {
111+
"row": 1,
112+
"col": 1
113+
}
114+
}]
115+
}]
116+
}`,
117+
},
118+
},
119+
}
120+
121+
createTempFile := func(t *testing.T, name string, content string) string {
122+
f, err := os.CreateTemp(t.TempDir(), name)
123+
require.NoError(t, err)
124+
t.Cleanup(func() { os.Remove(f.Name()) })
125+
_, err = f.WriteString(content)
126+
require.NoError(t, err)
127+
return f.Name()
128+
}
129+
130+
for _, test := range tests {
131+
test := test
132+
t.Run(test.name, func(t *testing.T) {
133+
policy := createTempFile(t, "policy-*.rego", test.policy)
134+
inputs := make([]string, len(test.inputs))
135+
for i, input := range test.inputs {
136+
f := createTempFile(t, "inputs-*.json", input)
137+
inputs[i] = f
138+
}
139+
140+
options := &EvalOptions{
141+
RulePath: policy,
142+
}
143+
144+
outputs, err := EvalWithOutputE(t, options, inputs, test.query)
145+
if test.isError {
146+
assert.Error(t, err)
147+
} else {
148+
assert.NoError(t, err)
149+
}
150+
for i, output := range test.outputs {
151+
require.JSONEq(t, output, outputs[i], "output for input: %d", i)
152+
}
153+
})
154+
}
155+
}

0 commit comments

Comments
 (0)