mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
topdown/rego: Add BuiltinErrorList support to rego package, add to eval command (#5487)
```
$ cat pol.rego
package play
this_errors(number) := result {
result := number / 0
}
this_errors_too(number) := result {
result := number / 0
}
res1 := this_errors(1)
res2 := this_errors_too(1)
$ go run main.go eval --show-builtin-errors -d pol.rego data.play
{
"errors": [
{
"message": "div: divide by zero",
"code": "eval_builtin_error",
"location": {
"file": "pol.rego",
"row": 4,
"col": 12
}
},
{
"message": "div: divide by zero",
"code": "eval_builtin_error",
"location": {
"file": "pol.rego",
"row": 8,
"col": 12
}
}
],
"result": [
{
"expressions": [
{
"value": {},
"text": "data.play",
"location": {
"row": 1,
"col": 1
}
}
]
}
]
}
```
Signed-off-by: Charlie Egan <charlieegan3@users.noreply.github.com>
This commit is contained in:
+42
-16
@@ -44,6 +44,7 @@ type evalCommandParams struct {
|
||||
disableIndexing bool
|
||||
disableEarlyExit bool
|
||||
strictBuiltinErrors bool
|
||||
showBuiltinErrors bool
|
||||
dataPaths repeatedStringFlag
|
||||
inputPath string
|
||||
imports repeatedStringFlag
|
||||
@@ -289,7 +290,8 @@ access.
|
||||
evalCommand.Flags().BoolVarP(¶ms.shallowInlining, "shallow-inlining", "", false, "disable inlining of rules that depend on unknowns")
|
||||
evalCommand.Flags().BoolVar(¶ms.disableIndexing, "disable-indexing", false, "disable indexing optimizations")
|
||||
evalCommand.Flags().BoolVar(¶ms.disableEarlyExit, "disable-early-exit", false, "disable 'early exit' optimizations")
|
||||
evalCommand.Flags().BoolVarP(¶ms.strictBuiltinErrors, "strict-builtin-errors", "", false, "treat built-in function errors as fatal")
|
||||
evalCommand.Flags().BoolVarP(¶ms.strictBuiltinErrors, "strict-builtin-errors", "", false, "treat the first built-in function error encountered as fatal")
|
||||
evalCommand.Flags().BoolVarP(¶ms.showBuiltinErrors, "show-builtin-errors", "", false, "collect and return all encountered built-in errors, built in errors are not fatal")
|
||||
evalCommand.Flags().BoolVarP(¶ms.instrument, "instrument", "", false, "enable query instrumentation metrics (implies --metrics)")
|
||||
evalCommand.Flags().BoolVarP(¶ms.profile, "profile", "", false, "perform expression profiling")
|
||||
evalCommand.Flags().VarP(¶ms.profileCriteria, "profile-sort", "", "set sort order of expression profiler results")
|
||||
@@ -376,6 +378,11 @@ func eval(args []string, params evalCommandParams, w io.Writer) (bool, error) {
|
||||
result.AggregatedMetrics = timersAggregated
|
||||
}
|
||||
|
||||
var builtInErrorCount int
|
||||
if ectx.params.showBuiltinErrors {
|
||||
builtInErrorCount = len(*(ectx.builtInErrorList))
|
||||
}
|
||||
|
||||
switch ectx.params.outputFormat.String() {
|
||||
case evalBindingsOutput:
|
||||
err = pr.Bindings(w, result)
|
||||
@@ -393,7 +400,11 @@ func eval(args []string, params evalCommandParams, w io.Writer) (bool, error) {
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if len(result.Errors) > 0 {
|
||||
} else if errorCount := len(result.Errors); errorCount > 0 && errorCount != builtInErrorCount {
|
||||
// if we only have built-in errors, we don't want to return an error. If
|
||||
// strict-builtin-errors is set the first built-in error will be returned
|
||||
// in a result error instead.
|
||||
|
||||
// If the rego package returned an error, return a special error here so
|
||||
// that the command doesn't print the same error twice. The error will
|
||||
// have been printed above by the presentation package.
|
||||
@@ -436,6 +447,11 @@ func evalOnce(ctx context.Context, ectx *evalContext) pr.Output {
|
||||
}
|
||||
|
||||
result.Errors = pr.NewOutputErrors(resultErr)
|
||||
if ectx.builtInErrorList != nil {
|
||||
for _, err := range *(ectx.builtInErrorList) {
|
||||
result.Errors = append(result.Errors, pr.NewOutputErrors(&err)...)
|
||||
}
|
||||
}
|
||||
|
||||
if ectx.params.explain != nil {
|
||||
switch ectx.params.explain.String() {
|
||||
@@ -473,13 +489,14 @@ func evalOnce(ctx context.Context, ectx *evalContext) pr.Output {
|
||||
}
|
||||
|
||||
type evalContext struct {
|
||||
params evalCommandParams
|
||||
metrics metrics.Metrics
|
||||
profiler *resettableProfiler
|
||||
cover *cover.Cover
|
||||
tracer *topdown.BufferTracer
|
||||
regoArgs []func(*rego.Rego)
|
||||
evalArgs []rego.EvalOption
|
||||
params evalCommandParams
|
||||
metrics metrics.Metrics
|
||||
profiler *resettableProfiler
|
||||
cover *cover.Cover
|
||||
tracer *topdown.BufferTracer
|
||||
regoArgs []func(*rego.Rego)
|
||||
evalArgs []rego.EvalOption
|
||||
builtInErrorList *[]topdown.Error
|
||||
}
|
||||
|
||||
func setupEval(args []string, params evalCommandParams) (*evalContext, error) {
|
||||
@@ -622,6 +639,14 @@ func setupEval(args []string, params evalCommandParams) (*evalContext, error) {
|
||||
|
||||
if params.strictBuiltinErrors {
|
||||
regoArgs = append(regoArgs, rego.StrictBuiltinErrors(true))
|
||||
if params.showBuiltinErrors {
|
||||
return nil, fmt.Errorf("cannot use --show-builtin-errors with --strict-builtin-errors, --strict-builtin-errors will return the first built-in error encountered immediately")
|
||||
}
|
||||
}
|
||||
|
||||
var builtInErrors []topdown.Error
|
||||
if params.showBuiltinErrors {
|
||||
regoArgs = append(regoArgs, rego.BuiltinErrorList(&builtInErrors))
|
||||
}
|
||||
|
||||
if params.capabilities != nil {
|
||||
@@ -633,13 +658,14 @@ func setupEval(args []string, params evalCommandParams) (*evalContext, error) {
|
||||
}
|
||||
|
||||
evalCtx := &evalContext{
|
||||
params: params,
|
||||
metrics: m,
|
||||
profiler: &rp,
|
||||
cover: c,
|
||||
tracer: tracer,
|
||||
regoArgs: regoArgs,
|
||||
evalArgs: evalArgs,
|
||||
params: params,
|
||||
metrics: m,
|
||||
profiler: &rp,
|
||||
cover: c,
|
||||
tracer: tracer,
|
||||
regoArgs: regoArgs,
|
||||
evalArgs: evalArgs,
|
||||
builtInErrorList: &builtInErrors,
|
||||
}
|
||||
|
||||
return evalCtx, nil
|
||||
|
||||
+2
-1
@@ -547,10 +547,11 @@ opa eval <query> [flags]
|
||||
--profile-sort string set sort order of expression profiler results
|
||||
-s, --schema string set schema file path or directory path
|
||||
--shallow-inlining disable inlining of rules that depend on unknowns
|
||||
--show-builtin-errors collect and return all encountered built-in errors, built in errors are not fatal
|
||||
--stdin read query from stdin
|
||||
-I, --stdin-input read input document from stdin
|
||||
-S, --strict enable compiler strict mode
|
||||
--strict-builtin-errors treat built-in function errors as fatal
|
||||
--strict-builtin-errors treat the first built-in function error encountered as fatal
|
||||
-t, --target {rego,wasm} set the runtime to exercise (default rego)
|
||||
--timeout duration set eval timeout (default unlimited)
|
||||
-u, --unknowns stringArray set paths to treat as unknown during partial evaluation (default [input])
|
||||
|
||||
@@ -528,6 +528,7 @@ type Rego struct {
|
||||
interQueryBuiltinCache cache.InterQueryCache
|
||||
ndBuiltinCache builtins.NDBCache
|
||||
strictBuiltinErrors bool
|
||||
builtinErrorList *[]topdown.Error
|
||||
resolvers []refResolver
|
||||
schemaSet *ast.SchemaSet
|
||||
target string // target type (wasm, rego, etc.)
|
||||
@@ -1055,6 +1056,13 @@ func StrictBuiltinErrors(yes bool) func(r *Rego) {
|
||||
}
|
||||
}
|
||||
|
||||
// BuiltinErrorList supplies an error slice to store built-in function errors.
|
||||
func BuiltinErrorList(list *[]topdown.Error) func(r *Rego) {
|
||||
return func(r *Rego) {
|
||||
r.builtinErrorList = list
|
||||
}
|
||||
}
|
||||
|
||||
// Resolver sets a Resolver for a specified ref path.
|
||||
func Resolver(ref ast.Ref, r resolver.Resolver) func(r *Rego) {
|
||||
return func(rego *Rego) {
|
||||
@@ -1959,6 +1967,7 @@ func (r *Rego) eval(ctx context.Context, ectx *EvalContext) (ResultSet, error) {
|
||||
WithEarlyExit(ectx.earlyExit).
|
||||
WithInterQueryBuiltinCache(ectx.interQueryBuiltinCache).
|
||||
WithStrictBuiltinErrors(r.strictBuiltinErrors).
|
||||
WithBuiltinErrorList(r.builtinErrorList).
|
||||
WithSeed(ectx.seed).
|
||||
WithPrintHook(ectx.printHook).
|
||||
WithDistributedTracingOpts(r.distributedTacingOpts)
|
||||
|
||||
@@ -2253,6 +2253,23 @@ func TestStrictBuiltinErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltinErrorList(t *testing.T) {
|
||||
var buf []topdown.Error
|
||||
|
||||
_, err := New(Query("1/0"), BuiltinErrorList(&buf)).Eval(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal("unexpected error")
|
||||
}
|
||||
|
||||
if len(buf) != 1 {
|
||||
t.Fatal("expected 1 error in buffer")
|
||||
}
|
||||
|
||||
if buf[0].Error() != "1/0: eval_builtin_error: div: divide by zero" {
|
||||
t.Fatal("expected divide by zero error but got:", buf[0].Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeSeedingOptions(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
+47
-4
@@ -54,6 +54,7 @@ type Query struct {
|
||||
interQueryBuiltinCache cache.InterQueryCache
|
||||
ndBuiltinCache builtins.NDBCache
|
||||
strictBuiltinErrors bool
|
||||
builtinErrorList *[]Error
|
||||
strictObjects bool
|
||||
printHook print.Hook
|
||||
tracingOpts tracing.Options
|
||||
@@ -256,6 +257,14 @@ func (q *Query) WithStrictBuiltinErrors(yes bool) *Query {
|
||||
return q
|
||||
}
|
||||
|
||||
// WithBuiltinErrorList supplies a pointer to an Error slice to store built-in function errors
|
||||
// encountered during evaluation. This error slice can be inspected after evaluation to determine
|
||||
// which built-in function errors occurred.
|
||||
func (q *Query) WithBuiltinErrorList(list *[]Error) *Query {
|
||||
q.builtinErrorList = list
|
||||
return q
|
||||
}
|
||||
|
||||
// WithResolver configures an external resolver to use for the given ref.
|
||||
func (q *Query) WithResolver(ref ast.Ref, r resolver.Resolver) *Query {
|
||||
q.external.Put(ref, r)
|
||||
@@ -419,8 +428,25 @@ func (q *Query) PartialRun(ctx context.Context) (partials []ast.Body, support []
|
||||
|
||||
support = e.saveSupport.List()
|
||||
|
||||
if q.strictBuiltinErrors && len(e.builtinErrors.errs) > 0 {
|
||||
err = e.builtinErrors.errs[0]
|
||||
if len(e.builtinErrors.errs) > 0 {
|
||||
if q.strictBuiltinErrors {
|
||||
err = e.builtinErrors.errs[0]
|
||||
} else if q.builtinErrorList != nil {
|
||||
// If a builtinErrorList has been supplied, we must use pointer indirection
|
||||
// to append to it. builtinErrorList is a slice pointer so that errors can be
|
||||
// appended to it without returning a new slice and changing the interface
|
||||
// of PartialRun.
|
||||
for _, err := range e.builtinErrors.errs {
|
||||
if tdError, ok := err.(*Error); ok {
|
||||
*(q.builtinErrorList) = append(*(q.builtinErrorList), *tdError)
|
||||
} else {
|
||||
*(q.builtinErrorList) = append(*(q.builtinErrorList), Error{
|
||||
Code: BuiltinErr,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range support {
|
||||
@@ -504,8 +530,25 @@ func (q *Query) Iter(ctx context.Context, iter func(QueryResult) error) error {
|
||||
return iter(qr)
|
||||
})
|
||||
|
||||
if q.strictBuiltinErrors && err == nil && len(e.builtinErrors.errs) > 0 {
|
||||
err = e.builtinErrors.errs[0]
|
||||
if len(e.builtinErrors.errs) > 0 {
|
||||
if q.strictBuiltinErrors {
|
||||
err = e.builtinErrors.errs[0]
|
||||
} else if q.builtinErrorList != nil {
|
||||
// If a builtinErrorList has been supplied, we must use pointer indirection
|
||||
// to append to it. builtinErrorList is a slice pointer so that errors can be
|
||||
// appended to it without returning a new slice and changing the interface
|
||||
// of Iter.
|
||||
for _, err := range e.builtinErrors.errs {
|
||||
if tdError, ok := err.(*Error); ok {
|
||||
*(q.builtinErrorList) = append(*(q.builtinErrorList), *tdError)
|
||||
} else {
|
||||
*(q.builtinErrorList) = append(*(q.builtinErrorList), Error{
|
||||
Code: BuiltinErr,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
q.metrics.Timer(metrics.RegoQueryEval).Stop()
|
||||
|
||||
Reference in New Issue
Block a user