From 39cc4ef87fc0c2c2cb320f3df91ab2aad1f4c2ff Mon Sep 17 00:00:00 2001 From: Torin Sandall Date: Sun, 11 Mar 2018 21:52:11 -0700 Subject: [PATCH] Add eval subcommand to run queries With opa eval, the --eval flag on opa run is redundant and can be removed. --- cmd/eval.go | 202 ++++++++++++++++++++++++++++++++++++++++ cmd/run.go | 17 ---- runtime/runtime.go | 16 +--- runtime/runtime_test.go | 19 ---- 4 files changed, 203 insertions(+), 51 deletions(-) create mode 100644 cmd/eval.go diff --git a/cmd/eval.go b/cmd/eval.go new file mode 100644 index 0000000000..e02e5bdbf9 --- /dev/null +++ b/cmd/eval.go @@ -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 ", + 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(¶ms.dataPath, "data", "d", "", "set data file or directory path") + evalCommand.Flags().StringVarP(¶ms.inputPath, "input", "i", "", "set input file path") + evalCommand.Flags().VarP(¶ms.imports, "import", "", "set query import(s)") + evalCommand.Flags().StringVarP(¶ms.pkg, "package", "", "", "set query package") + evalCommand.Flags().BoolVarP(¶ms.stdin, "stdin", "", false, "read query from stdin") + evalCommand.Flags().BoolVarP(¶ms.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 +} diff --git a/cmd/run.go b/cmd/run.go index 989da9ab46..349bddf8e8 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -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: : - -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(¶ms.Eval, "eval", "e", "", "evaluate, print, exit") runCommand.Flags().StringVarP(¶ms.HistoryPath, "history", "H", historyPath(), "set path of history file") runCommand.Flags().StringVarP(¶ms.Addr, "addr", "a", defaultAddr, "set listening address of the server") runCommand.Flags().StringVarP(¶ms.InsecureAddr, "insecure-addr", "", "", "set insecure listening address of the server") diff --git a/runtime/runtime.go b/runtime/runtime.go index 48ba7fa266..464260ac36 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -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 { diff --git a/runtime/runtime_test.go b/runtime/runtime_test.go index 26c1aa09c3..c9b09da9ad 100644 --- a/runtime/runtime_test.go +++ b/runtime/runtime_test.go @@ -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()