Add eval subcommand to run queries

With opa eval, the --eval flag on opa run is redundant and can be
removed.
This commit is contained in:
Torin Sandall
2018-03-11 21:52:11 -07:00
parent 2b3b3faeed
commit 39cc4ef87f
4 changed files with 203 additions and 51 deletions
+202
View File
@@ -0,0 +1,202 @@
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package cmd
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/loader"
"github.com/open-policy-agent/opa/metrics"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/storage/inmem"
"github.com/open-policy-agent/opa/topdown"
"github.com/open-policy-agent/opa/util"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type evalCommandParams struct {
dataPath string
inputPath string
imports repeatedStringFlag
pkg string
stdin bool
explain *util.EnumFlag
metrics bool
}
const (
explainModeOff = ""
explainModeFull = "full"
)
type evalResult struct {
Result rego.ResultSet `json:"result,omitempty"`
Explanation []string `json:"explanation,omitempty"`
Metrics map[string]interface{} `json:"metrics,omitempty"`
}
func init() {
var params evalCommandParams
params.explain = util.NewEnumFlag(explainModeOff, []string{explainModeFull})
evalCommand := &cobra.Command{
Use: "eval <query>",
Short: "Evaluate a Rego query",
Long: `Evaluate a Rego query and print the result.
TODO show how to use eval command`,
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 && params.stdin {
return errors.New("specify query argument or --stdin but not both")
} else if len(args) == 0 && !params.stdin {
return errors.New("specify query argument or --stdin")
} else if len(args) > 1 {
return errors.New("specify at most one query argument")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
if err := eval(args, params); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
},
}
evalCommand.Flags().StringVarP(&params.dataPath, "data", "d", "", "set data file or directory path")
evalCommand.Flags().StringVarP(&params.inputPath, "input", "i", "", "set input file path")
evalCommand.Flags().VarP(&params.imports, "import", "", "set query import(s)")
evalCommand.Flags().StringVarP(&params.pkg, "package", "", "", "set query package")
evalCommand.Flags().BoolVarP(&params.stdin, "stdin", "", false, "read query from stdin")
evalCommand.Flags().BoolVarP(&params.metrics, "metrics", "", false, "report query performance metrics")
evalCommand.Flags().VarP(params.explain, "explain", "", "enable query explainations")
RootCommand.AddCommand(evalCommand)
}
func eval(args []string, params evalCommandParams) (err error) {
var query string
if params.stdin {
bs, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return err
}
query = string(bs)
} else {
query = args[0]
}
regoArgs := []func(*rego.Rego){rego.Query(query)}
if len(params.imports.v) > 0 {
regoArgs = append(regoArgs, rego.Imports(params.imports.v))
}
if params.pkg != "" {
regoArgs = append(regoArgs, rego.Package(params.pkg))
}
if params.dataPath != "" {
loadResult, err := loader.All([]string{params.dataPath})
if err != nil {
return err
}
regoArgs = append(regoArgs, rego.Store(inmem.NewFromObject(loadResult.Documents)))
for _, file := range loadResult.Modules {
regoArgs = append(regoArgs, rego.Module(file.Name, string(file.Raw)))
}
}
if params.inputPath != "" {
bs, err := ioutil.ReadFile(params.inputPath)
if err != nil {
return err
}
term, err := ast.ParseTerm(string(bs))
if err != nil {
return err
}
regoArgs = append(regoArgs, rego.ParsedInput(term.Value))
}
var tracer *topdown.BufferTracer
switch params.explain.String() {
case explainModeFull:
tracer = topdown.NewBufferTracer()
regoArgs = append(regoArgs, rego.Tracer(tracer))
}
var m metrics.Metrics
if params.metrics {
m = metrics.New()
regoArgs = append(regoArgs, rego.Metrics(m))
}
eval := rego.New(regoArgs...)
ctx := context.Background()
rs, err := eval.Eval(ctx)
if err != nil {
return err
}
result := evalResult{
Result: rs,
}
if params.explain.String() != explainModeOff {
var traceBuffer bytes.Buffer
topdown.PrettyTrace(&traceBuffer, *tracer)
result.Explanation = strings.Split(traceBuffer.String(), "\n")
}
if params.metrics {
result.Metrics = m.All()
}
bs, err := json.MarshalIndent(result, "", " ")
if err != nil {
return err
}
fmt.Println(string(bs))
return nil
}
type repeatedStringFlag struct {
v []string
}
func newRepeatedStringFlag() *repeatedStringFlag {
f := &repeatedStringFlag{}
return f
}
func (f *repeatedStringFlag) Type() string {
return "string"
}
func (f *repeatedStringFlag) String() string {
return strings.Join(f.v, ",")
}
func (f *repeatedStringFlag) Set(s string) error {
f.v = append(f.v, s)
return nil
}
-17
View File
@@ -63,10 +63,6 @@ To run the server:
$ opa run -s
To evaluate a query from the command line:
$ opa run -e 'data.repl.version[key] = value'
The 'run' command starts an instance of the OPA runtime. The OPA runtime can be
started as an interactive shell or a server.
@@ -89,18 +85,6 @@ Data file and directory paths can be prefixed with the desired destination in
the data document with the following syntax:
<dotted-path>:<file-path>
For example:
$ echo "[1,2,3]" > example.json
$ opa run -e 'data.foo' foo.bar:./example.json
{
"bar": [
1,
2,
3
]
}
`,
Run: func(cmd *cobra.Command, args []string) {
@@ -137,7 +121,6 @@ For example:
}
runCommand.Flags().BoolVarP(&serverMode, "server", "s", false, "start the runtime in server mode")
runCommand.Flags().StringVarP(&params.Eval, "eval", "e", "", "evaluate, print, exit")
runCommand.Flags().StringVarP(&params.HistoryPath, "history", "H", historyPath(), "set path of history file")
runCommand.Flags().StringVarP(&params.Addr, "addr", "a", defaultAddr, "set listening address of the server")
runCommand.Flags().StringVarP(&params.InsecureAddr, "insecure-addr", "", "", "set insecure listening address of the server")
+1 -15
View File
@@ -46,9 +46,6 @@ type Params struct {
// is nil, the server will NOT use TLS.
Certificate *tls.Certificate
// Eval is a string to evaluate in the REPL.
Eval string
// HistoryPath is the filename to store the interactive shell user
// input history.
HistoryPath string
@@ -206,18 +203,7 @@ func (rt *Runtime) StartREPL(ctx context.Context) {
}
}
if rt.Params.Eval == "" {
repl.Loop(ctx)
} else {
repl.DisableUndefinedOutput(true)
repl.DisableMultiLineBuffering(true)
if err := repl.OneShot(ctx, rt.Params.Eval); err != nil {
fmt.Fprintln(rt.Params.Output, "error:", err)
os.Exit(1)
}
}
repl.Loop(ctx)
}
func (rt *Runtime) startWatcher(ctx context.Context, paths []string, onReload func(time.Duration, error)) error {
-19
View File
@@ -21,25 +21,6 @@ import (
"github.com/open-policy-agent/opa/util/test"
)
func TestEval(t *testing.T) {
ctx := context.Background()
params := NewParams()
var buffer bytes.Buffer
params.Output = &buffer
params.OutputFormat = "json"
params.Eval = `a = b; a = 1; c = 2; c > b`
rt, err := NewRuntime(ctx, params)
if err != nil {
t.Fatal(err)
}
rt.StartREPL(ctx)
expected := util.MustUnmarshalJSON([]byte(`[{"a": 1, "b": 1, "c": 2}]`))
result := util.MustUnmarshalJSON(buffer.Bytes())
if !reflect.DeepEqual(expected, result) {
t.Fatalf("Expected %v but got: %v", expected, result)
}
}
func TestInit(t *testing.T) {
ctx := context.Background()