Add a --format=raw option for eval (#3207)

This is a very simple addition but it's proven extremely useful to us since
it means you can now use `opa` easily in bash scripts, e.g.:

```bash
ACCOUNT_ID="$(opa eval -d accounts.rego --format raw "data.accounts.account")"
```

This means `accounts.rego` can be used by policies as well as e.g. deploy
scripts.

The naming and functionality is inspired by the `-r` flag from `jq`.  In fact,
`jq` can be used to replicate this behaviour, but it is a bit nicer to not rely
on that system dependency.

When multiple queries and expressions are given, they are printed in a simple
table format using single newlines and spaces since that is consistent and plays
nice with `bash`.

Signed-off-by: Jasper Van der Jeugt <jasper@fugue.co>
This commit is contained in:
Jasper Van der Jeugt
2021-03-04 08:26:48 +01:00
committed by GitHub
parent 0557c5f4d4
commit ef9814c9ab
3 changed files with 112 additions and 0 deletions
+4
View File
@@ -71,6 +71,7 @@ func newEvalCommandParams() evalCommandParams {
evalBindingsOutput,
evalPrettyOutput,
evalSourceOutput,
evalRawOutput,
}),
explain: newExplainFlag([]string{explainModeOff, explainModeFull, explainModeNotes, explainModeFails}),
target: util.NewEnumFlag(compile.TargetRego, []string{compile.TargetRego, compile.TargetWasm}),
@@ -118,6 +119,7 @@ const (
evalBindingsOutput = "bindings"
evalPrettyOutput = "pretty"
evalSourceOutput = "source"
evalRawOutput = "raw"
// number of profile results to return by default
defaultProfileLimit = 10
@@ -335,6 +337,8 @@ func eval(args []string, params evalCommandParams, w io.Writer) (bool, error) {
err = pr.Pretty(w, result)
case evalSourceOutput:
err = pr.Source(w, result)
case evalRawOutput:
err = pr.Raw(w, result)
default:
err = pr.JSON(w, result)
}
+33
View File
@@ -343,6 +343,39 @@ func Source(w io.Writer, r Output) error {
return nil
}
// Raw prints the values from r to w. Each result is written on a separate
// line, and the expressions are separated by spaces. If the values are
// strings, they are written directly rather than formatted as compact
// JSON strings. This output format makes OPA useful in a scripting context.
func Raw(w io.Writer, r Output) error {
if r.Errors != nil {
return prettyError(w, r.Errors)
}
for _, rs := range r.Result {
for i, expr := range rs.Expressions {
if str, ok := expr.Value.(string); ok {
fmt.Fprint(w, str)
} else {
bytes, err := json.Marshal(expr.Value)
if err != nil {
return err
}
fmt.Fprint(w, string(bytes))
}
if i+1 >= len(rs.Expressions) {
fmt.Fprintln(w, "")
} else {
fmt.Fprint(w, " ")
}
}
}
return nil
}
func prettyError(w io.Writer, err error) error {
_, err = fmt.Fprintln(w, err)
return err
@@ -388,3 +388,78 @@ p = 1
}
}
func TestRaw(t *testing.T) {
tests := []struct {
note string
output Output
want string
}{
{
note: "simple single string",
output: Output{
Result: []rego.Result{
{
Expressions: []*rego.ExpressionValue{
{Value: "Hello world"},
},
},
},
},
want: "Hello world\n",
},
{
note: "table format",
output: Output{
Result: []rego.Result{
{
Expressions: []*rego.ExpressionValue{
{Value: "one"},
{Value: 1},
},
},
{
Expressions: []*rego.ExpressionValue{
{Value: "two"},
{Value: 2},
},
},
},
},
want: "one 1\ntwo 2\n",
},
{
note: "compound values",
output: Output{
Result: []rego.Result{
{
Expressions: []*rego.ExpressionValue{
{Value: []interface{}{"one"}},
{Value: map[string]interface{}{
"key": []interface{}{},
}},
},
},
},
},
want: "[\"one\"] {\"key\":[]}\n",
},
{
note: "error",
output: Output{
Errors: NewOutputErrors(fmt.Errorf("boom")),
},
want: "1 error occurred: boom\n",
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
buf := new(bytes.Buffer)
Raw(buf, tc.output)
if buf.String() != tc.want {
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", tc.want, buf.String())
}
})
}
}