mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-23 00:34:48 -06:00
4c13c6cc9f
Funnily, this started out as an attempt to look into issues reported with compiling large policy sets... before I realized that it isn't likely *this* compiler that has perf issues, but the one that "compiles" bundles as part of activation. So while these fixes likely does little to address that, there are still some rather nice improvements here, where the big ones as ususal are mostly just wins from avoiding work where it's possible. For benchmarking I've used Regal's embedded bundle, which isn't great to use over time, as it's a moving target. But since it's a pretty extensive bundle and one that covers most features of OPA, it's at least good for 1:1 comparisons when testing perf improvements. ``` // 66555594 ns/op 50239492 B/op 1083664 allocs/op - main // 62569440 ns/op 38723015 B/op 944277 allocs/op - compiler-optimizations pr ``` The B/op / alloc_space improvement is particularly nice here. What's noteworthy is how relatively little impact that has on performance in this case. That may be surprising but aligns pretty well with my previous experience of Go code where a lot of time is spend in recursive walks — that simply takes time, no matter how much you optimize. Oh well, less memory allocated for this is more memory to spend elsewhere. (I'm adding the benchmark used below to Regal in a parallel PR) Signed-off-by: Anders Eknert <anders@styra.com>
897 lines
27 KiB
Go
897 lines
27 KiB
Go
// 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"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/open-policy-agent/opa/cmd/formats"
|
|
"github.com/open-policy-agent/opa/cmd/internal/env"
|
|
fileurl "github.com/open-policy-agent/opa/internal/file/url"
|
|
pr "github.com/open-policy-agent/opa/internal/presentation"
|
|
"github.com/open-policy-agent/opa/internal/runtime"
|
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
"github.com/open-policy-agent/opa/v1/ast/location"
|
|
"github.com/open-policy-agent/opa/v1/bundle"
|
|
"github.com/open-policy-agent/opa/v1/compile"
|
|
"github.com/open-policy-agent/opa/v1/cover"
|
|
"github.com/open-policy-agent/opa/v1/loader"
|
|
"github.com/open-policy-agent/opa/v1/metrics"
|
|
"github.com/open-policy-agent/opa/v1/profiler"
|
|
"github.com/open-policy-agent/opa/v1/rego"
|
|
"github.com/open-policy-agent/opa/v1/topdown"
|
|
"github.com/open-policy-agent/opa/v1/topdown/lineage"
|
|
"github.com/open-policy-agent/opa/v1/util"
|
|
)
|
|
|
|
var errIllegalUnknownsArg = errors.New("illegal argument with --unknowns, specify string with one or more --unknowns")
|
|
|
|
type evalCommandParams struct {
|
|
capabilities *capabilitiesFlag
|
|
coverage bool
|
|
partial bool
|
|
unknowns []string
|
|
disableInlining []string
|
|
nondeterministicBuiltions bool
|
|
shallowInlining bool
|
|
disableIndexing bool
|
|
disableEarlyExit bool
|
|
strictBuiltinErrors bool
|
|
showBuiltinErrors bool
|
|
dataPaths repeatedStringFlag
|
|
inputPath string
|
|
imports repeatedStringFlag
|
|
pkg string
|
|
stdin bool
|
|
stdinInput bool
|
|
explain *util.EnumFlag
|
|
metrics bool
|
|
instrument bool
|
|
ignore []string
|
|
outputFormat *util.EnumFlag
|
|
profile bool
|
|
profileCriteria repeatedStringFlag
|
|
profileLimit intFlag
|
|
count int
|
|
prettyLimit intFlag
|
|
fail bool
|
|
failDefined bool
|
|
bundlePaths repeatedStringFlag
|
|
schema *schemaFlags
|
|
target *util.EnumFlag
|
|
timeout time.Duration
|
|
optimizationLevel int
|
|
entrypoints repeatedStringFlag
|
|
strict bool
|
|
v0Compatible bool
|
|
v1Compatible bool
|
|
traceVarValues bool
|
|
ReadAstValuesFromStore bool
|
|
}
|
|
|
|
func (p *evalCommandParams) regoVersion() ast.RegoVersion {
|
|
if p.v0Compatible {
|
|
return ast.RegoV0
|
|
} else if p.v1Compatible {
|
|
return ast.RegoV1
|
|
}
|
|
return ast.DefaultRegoVersion
|
|
}
|
|
|
|
func newEvalCommandParams() evalCommandParams {
|
|
return evalCommandParams{
|
|
capabilities: newCapabilitiesFlag(),
|
|
outputFormat: formats.Flag(
|
|
formats.JSON,
|
|
formats.Values,
|
|
formats.Bindings,
|
|
formats.Pretty,
|
|
formats.Source,
|
|
formats.Raw,
|
|
formats.Discard,
|
|
),
|
|
explain: newExplainFlag([]string{explainModeOff, explainModeFull, explainModeNotes, explainModeFails, explainModeDebug}),
|
|
target: util.NewEnumFlag(compile.TargetRego, []string{compile.TargetRego, compile.TargetWasm}),
|
|
count: 1,
|
|
profileCriteria: newrepeatedStringFlag([]string{}),
|
|
profileLimit: newIntFlag(defaultProfileLimit),
|
|
prettyLimit: newIntFlag(defaultPrettyLimit),
|
|
schema: &schemaFlags{},
|
|
}
|
|
}
|
|
|
|
func validateEvalParams(p *evalCommandParams, cmdArgs []string) error {
|
|
if len(cmdArgs) > 0 && p.stdin {
|
|
return errors.New("specify query argument or --stdin but not both")
|
|
} else if len(cmdArgs) == 0 && !p.stdin {
|
|
return errors.New("specify query argument or --stdin")
|
|
} else if len(cmdArgs) > 1 {
|
|
return errors.New("specify at most one query argument")
|
|
}
|
|
if p.stdin && p.stdinInput {
|
|
return errors.New("specify --stdin or --stdin-input but not both")
|
|
}
|
|
if p.stdinInput && p.inputPath != "" {
|
|
return errors.New("specify --stdin-input or --input but not both")
|
|
}
|
|
if p.fail && p.failDefined {
|
|
return errors.New("specify --fail or --fail-defined but not both")
|
|
}
|
|
of := p.outputFormat.String()
|
|
if p.partial && of != formats.Pretty && of != formats.JSON && of != formats.Source {
|
|
return errors.New("invalid output format for partial evaluation")
|
|
} else if !p.partial && of == formats.Source {
|
|
return errors.New("invalid output format for evaluation")
|
|
}
|
|
|
|
// check if illegal arguments is passed with unknowns flag
|
|
for _, unknwn := range p.unknowns {
|
|
if unknwn == "input" {
|
|
continue
|
|
}
|
|
term, err := ast.ParseTerm(unknwn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch term.Value.(type) {
|
|
case ast.Ref:
|
|
return nil
|
|
default:
|
|
return errIllegalUnknownsArg
|
|
}
|
|
}
|
|
|
|
if p.optimizationLevel > 0 {
|
|
if len(p.dataPaths.v) > 0 && p.bundlePaths.isFlagSet() {
|
|
return errors.New("specify either --data or --bundle flag with optimization level greater than 0")
|
|
}
|
|
}
|
|
|
|
if p.profileLimit.isFlagSet() || p.profileCriteria.isFlagSet() {
|
|
p.profile = true
|
|
}
|
|
if p.profile {
|
|
p.metrics = true
|
|
}
|
|
if p.instrument {
|
|
p.metrics = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
// number of profile results to return by default
|
|
defaultProfileLimit = 10
|
|
defaultPrettyLimit = 80
|
|
)
|
|
|
|
type regoError struct {
|
|
wrapped error
|
|
}
|
|
|
|
func (regoError) Error() string {
|
|
return "rego"
|
|
}
|
|
|
|
func (r regoError) Unwrap() error {
|
|
return r.wrapped
|
|
}
|
|
|
|
func initEval(root *cobra.Command, _ string) {
|
|
executable := root.Name()
|
|
|
|
params := newEvalCommandParams()
|
|
|
|
evalCommand := &cobra.Command{
|
|
Use: "eval <query>",
|
|
Short: "Evaluate a Rego query",
|
|
Long: `Evaluate a Rego query and print the result.`,
|
|
Example: `
|
|
|
|
To evaluate a simple query:
|
|
|
|
$ ` + executable + ` eval 'x := 1; y := 2; x < y'
|
|
|
|
To evaluate a query against JSON data:
|
|
|
|
$ ` + executable + ` eval --data data.json 'name := data.names[_]'
|
|
|
|
To evaluate a query against JSON data supplied with a file:// URL:
|
|
|
|
$ ` + executable + ` eval --data file:///path/to/file.json 'data'
|
|
|
|
|
|
File & Bundle Loading
|
|
---------------------
|
|
|
|
The --bundle flag will load data files and Rego files contained
|
|
in the bundle specified by the path. It can be either a
|
|
compressed tar archive bundle file or a directory tree.
|
|
|
|
$ ` + executable + ` eval --bundle /some/path 'data'
|
|
|
|
Where /some/path contains:
|
|
|
|
foo/
|
|
|
|
|
+-- bar/
|
|
| |
|
|
| +-- data.json
|
|
|
|
|
+-- baz.rego
|
|
|
|
|
+-- manifest.yaml
|
|
|
|
The JSON file 'foo/bar/data.json' would be loaded and rooted under
|
|
'data.foo.bar' and the 'foo/baz.rego' would be loaded and rooted under the
|
|
package path contained inside the file. Only data files named data.json or
|
|
data.yaml will be loaded. In the example above the manifest.yaml would be
|
|
ignored.
|
|
|
|
See https://www.openpolicyagent.org/docs/latest/management-bundles/ for more details
|
|
on bundle directory structures.
|
|
|
|
The --data flag can be used to recursively load ALL *.rego, *.json, and
|
|
*.yaml files under the specified directory.
|
|
|
|
The -O flag controls the optimization level. By default, optimization is disabled (-O=0).
|
|
When optimization is enabled the 'eval' command generates a bundle from the files provided
|
|
with either the --bundle or --data flag. This bundle is semantically equivalent to the input
|
|
files however the structure of the files in the bundle may have been changed by rewriting, inlining,
|
|
pruning, etc. This resulting optimized bundle is used to evaluate the query. If optimization is
|
|
enabled at least one entrypoint must be supplied, either via the -e option, or via entrypoint
|
|
metadata annotations.
|
|
|
|
Output Formats
|
|
--------------
|
|
|
|
Set the output format with the --format flag.
|
|
|
|
--format=json : output raw query results as JSON
|
|
--format=values : output line separated JSON arrays containing expression values
|
|
--format=bindings : output line separated JSON objects containing variable bindings
|
|
--format=pretty : output query results in a human-readable format
|
|
--format=source : output partial evaluation results in a source format
|
|
--format=raw : output the values from query results in a scripting friendly format
|
|
--format=discard : output the result field as "discarded" when non-nil
|
|
|
|
Schema
|
|
------
|
|
|
|
The -s/--schema flag provides one or more JSON Schemas used to validate references to the input or data documents.
|
|
Loads a single JSON file, applying it to the input document; or all the schema files under the specified directory.
|
|
|
|
$ ` + executable + ` eval --data policy.rego --input input.json --schema schema.json
|
|
$ ` + executable + ` eval --data policy.rego --input input.json --schema schemas/
|
|
|
|
Capabilities
|
|
------------
|
|
|
|
When passing a capabilities definition file via --capabilities, one can restrict which
|
|
hosts remote schema definitions can be retrieved from. For example, a capabilities.json
|
|
containing
|
|
|
|
{
|
|
"builtins": [ ... ],
|
|
"allow_net": [ "kubernetesjsonschema.dev" ]
|
|
}
|
|
|
|
would disallow fetching remote schemas from any host but "kubernetesjsonschema.dev".
|
|
Setting allow_net to an empty array would prohibit fetching any remote schemas.
|
|
|
|
Not providing a capabilities file, or providing a file without an allow_net key, will
|
|
permit fetching remote schemas from any host.
|
|
|
|
Note that the metaschemas http://json-schema.org/draft-04/schema, http://json-schema.org/draft-06/schema,
|
|
and http://json-schema.org/draft-07/schema, are always available, even without network
|
|
access.
|
|
`,
|
|
|
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
|
if err := validateEvalParams(¶ms, args); err != nil {
|
|
return err
|
|
}
|
|
return env.CmdFlags.CheckEnvironmentVariables(cmd)
|
|
},
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cmd.SilenceErrors = true
|
|
cmd.SilenceUsage = true
|
|
|
|
defined, err := eval(args, params, os.Stdout)
|
|
if err != nil {
|
|
if _, ok := err.(regoError); !ok {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
}
|
|
return newExitErrorWrap(2, err)
|
|
}
|
|
|
|
if (params.fail && !defined) || (params.failDefined && defined) {
|
|
return newExitError(1)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// Eval specific flags
|
|
evalCommand.Flags().BoolVarP(¶ms.coverage, "coverage", "", false, "report coverage")
|
|
evalCommand.Flags().StringArrayVarP(¶ms.disableInlining, "disable-inlining", "", []string{}, "set paths of documents to exclude from inlining")
|
|
evalCommand.Flags().BoolVarP(¶ms.shallowInlining, "shallow-inlining", "", false, "disable inlining of rules that depend on unknowns")
|
|
evalCommand.Flags().BoolVarP(¶ms.nondeterministicBuiltions, "nondeterminstic-builtins", "", false, "evaluate nondeterministic builtins (if all arguments are known) during partial eval")
|
|
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 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. Accepts: total_time_ns, num_eval, num_redo, num_gen_expr, file, line. This flag can be repeated.")
|
|
evalCommand.Flags().VarP(¶ms.profileLimit, "profile-limit", "", "set number of profiling results to show")
|
|
evalCommand.Flags().VarP(¶ms.prettyLimit, "pretty-limit", "", "set limit after which pretty output gets truncated")
|
|
evalCommand.Flags().BoolVarP(¶ms.failDefined, "fail-defined", "", false, "exits with non-zero exit code on defined/non-empty result and errors")
|
|
evalCommand.Flags().DurationVar(¶ms.timeout, "timeout", 0, "set eval timeout (default unlimited)")
|
|
evalCommand.Flags().IntVarP(¶ms.optimizationLevel, "optimize", "O", 0, "set optimization level")
|
|
evalCommand.Flags().VarP(¶ms.entrypoints, "entrypoint", "e", "set slash separated entrypoint path")
|
|
evalCommand.Flags().BoolVar(¶ms.traceVarValues, "var-values", false, "show local variable values in pretty trace output")
|
|
|
|
// Shared flags
|
|
addCapabilitiesFlag(evalCommand.Flags(), params.capabilities)
|
|
addPartialFlag(evalCommand.Flags(), ¶ms.partial, false)
|
|
addUnknownsFlag(evalCommand.Flags(), ¶ms.unknowns, []string{"input"})
|
|
addFailFlag(evalCommand.Flags(), ¶ms.fail, false)
|
|
addDataFlag(evalCommand.Flags(), ¶ms.dataPaths)
|
|
addBundleFlag(evalCommand.Flags(), ¶ms.bundlePaths)
|
|
addInputFlag(evalCommand.Flags(), ¶ms.inputPath)
|
|
addImportFlag(evalCommand.Flags(), ¶ms.imports)
|
|
addPackageFlag(evalCommand.Flags(), ¶ms.pkg)
|
|
addQueryStdinFlag(evalCommand.Flags(), ¶ms.stdin)
|
|
addInputStdinFlag(evalCommand.Flags(), ¶ms.stdinInput)
|
|
addMetricsFlag(evalCommand.Flags(), ¶ms.metrics, false)
|
|
addOutputFormat(evalCommand.Flags(), params.outputFormat)
|
|
addIgnoreFlag(evalCommand.Flags(), ¶ms.ignore)
|
|
setExplainFlag(evalCommand.Flags(), params.explain)
|
|
addSchemaFlags(evalCommand.Flags(), params.schema)
|
|
addTargetFlag(evalCommand.Flags(), params.target)
|
|
addCountFlag(evalCommand.Flags(), ¶ms.count, "benchmark")
|
|
addStrictFlag(evalCommand.Flags(), ¶ms.strict, false)
|
|
addV0CompatibleFlag(evalCommand.Flags(), ¶ms.v0Compatible, false)
|
|
addV1CompatibleFlag(evalCommand.Flags(), ¶ms.v1Compatible, false)
|
|
addReadAstValuesFromStoreFlag(evalCommand.Flags(), ¶ms.ReadAstValuesFromStore, false)
|
|
|
|
root.AddCommand(evalCommand)
|
|
}
|
|
|
|
func eval(args []string, params evalCommandParams, w io.Writer) (bool, error) {
|
|
ctx := context.Background()
|
|
if params.timeout != 0 {
|
|
var cancel func()
|
|
ctx, cancel = context.WithTimeout(ctx, params.timeout)
|
|
defer cancel()
|
|
}
|
|
|
|
ectx, err := setupEval(args, params)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
ectx.regoArgs = append(ectx.regoArgs,
|
|
rego.EnablePrintStatements(true),
|
|
rego.PrintHook(topdown.NewPrintHook(os.Stderr)))
|
|
|
|
results := make([]pr.Output, ectx.params.count)
|
|
profiles := make([][]profiler.ExprStats, ectx.params.count)
|
|
timers := make([]map[string]any, ectx.params.count)
|
|
|
|
for i := range ectx.params.count {
|
|
results[i] = evalOnce(ctx, ectx)
|
|
profiles[i] = results[i].Profile
|
|
if ts, ok := results[i].Metrics.(metrics.TimerMetrics); ok {
|
|
timers[i] = ts.Timers()
|
|
}
|
|
}
|
|
|
|
result := results[0]
|
|
|
|
if ectx.params.count > 1 {
|
|
result.Profile = nil
|
|
result.Metrics = nil
|
|
result.AggregatedProfile = profiler.AggregateProfiles(profiles...)
|
|
timersAggregated := map[string]any{}
|
|
for name := range timers[0] {
|
|
var vals []int64
|
|
for _, t := range timers {
|
|
val, ok := t[name].(int64)
|
|
if !ok {
|
|
return false, fmt.Errorf("missing timer for %s", name)
|
|
}
|
|
vals = append(vals, val)
|
|
}
|
|
timersAggregated[name] = metrics.Statistics(vals...)
|
|
}
|
|
result.AggregatedMetrics = timersAggregated
|
|
}
|
|
|
|
var builtInErrorCount int
|
|
if ectx.params.showBuiltinErrors {
|
|
builtInErrorCount = len(*(ectx.builtInErrorList))
|
|
}
|
|
|
|
switch ectx.params.outputFormat.String() {
|
|
case formats.Bindings:
|
|
err = pr.Bindings(w, result)
|
|
case formats.Values:
|
|
err = pr.Values(w, result)
|
|
case formats.Pretty:
|
|
err = pr.PrettyWithOptions(w, result, pr.PrettyOptions{
|
|
TraceOpts: topdown.PrettyTraceOptions{
|
|
Locations: true,
|
|
ExprVariables: ectx.params.traceVarValues,
|
|
},
|
|
})
|
|
case formats.Source:
|
|
err = pr.Source(w, result)
|
|
case formats.Raw:
|
|
err = pr.Raw(w, result)
|
|
case formats.Discard:
|
|
err = pr.Discard(w, result)
|
|
default:
|
|
err = pr.JSON(w, result)
|
|
}
|
|
|
|
if err != nil {
|
|
return false, err
|
|
} 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.
|
|
return false, regoError{wrapped: result.Errors}
|
|
} else if len(result.Result) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func evalOnce(ctx context.Context, ectx *evalContext) pr.Output {
|
|
var result pr.Output
|
|
var resultErr error
|
|
var parsedModules map[string]*ast.Module
|
|
|
|
if ectx.metrics != nil {
|
|
ectx.metrics.Clear()
|
|
}
|
|
if ectx.profiler != nil {
|
|
ectx.profiler.reset()
|
|
}
|
|
r := rego.New(append(ectx.regoArgs, rego.CompilerHook(func(c *ast.Compiler) {
|
|
ctx = ast.WithCompiler(ctx, c)
|
|
}))...)
|
|
|
|
if !ectx.params.partial {
|
|
var pq rego.PreparedEvalQuery
|
|
pq, resultErr = r.PrepareForEval(ctx)
|
|
if resultErr == nil {
|
|
parsedModules = pq.Modules()
|
|
result.Result, resultErr = pq.Eval(ctx, ectx.evalArgs...)
|
|
}
|
|
} else {
|
|
var pq rego.PreparedPartialQuery
|
|
pq, resultErr = r.PrepareForPartial(ctx)
|
|
if resultErr == nil {
|
|
parsedModules = pq.Modules()
|
|
result.Partial, resultErr = pq.Partial(ctx, ectx.evalArgs...)
|
|
resetExprLocations(result.Partial)
|
|
}
|
|
}
|
|
|
|
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() {
|
|
case explainModeDebug:
|
|
result.Explanation = lineage.Debug(*(ectx.tracer))
|
|
case explainModeFull:
|
|
result.Explanation = lineage.Full(*(ectx.tracer))
|
|
case explainModeNotes:
|
|
result.Explanation = lineage.Notes(*(ectx.tracer))
|
|
case explainModeFails:
|
|
result.Explanation = lineage.Fails(*(ectx.tracer))
|
|
}
|
|
}
|
|
|
|
if ectx.metrics != nil {
|
|
result.Metrics = ectx.metrics
|
|
}
|
|
|
|
if ectx.params.profile {
|
|
sortOrder := pr.DefaultProfileSortOrder
|
|
|
|
if len(ectx.params.profileCriteria.v) != 0 {
|
|
sortOrder = getProfileSortOrder(strings.Split(ectx.params.profileCriteria.String(), ","))
|
|
}
|
|
|
|
result.Profile = ectx.profiler.p.ReportTopNResults(ectx.params.profileLimit.v, sortOrder)
|
|
}
|
|
|
|
if ectx.params.coverage {
|
|
report := ectx.cover.Report(parsedModules)
|
|
result.Coverage = &report
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
type evalContext struct {
|
|
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) {
|
|
var query string
|
|
|
|
if params.stdin {
|
|
bs, err := io.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query = string(bs)
|
|
} else {
|
|
query = args[0]
|
|
}
|
|
|
|
info, err := runtime.Term(runtime.Params{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
regoArgs := []func(*rego.Rego){
|
|
rego.Query(query),
|
|
rego.Runtime(info),
|
|
rego.SetRegoVersion(params.regoVersion()),
|
|
rego.StoreReadAST(params.ReadAstValuesFromStore),
|
|
rego.SkipBundleVerification(true),
|
|
}
|
|
|
|
evalArgs := []rego.EvalOption{
|
|
rego.EvalRuleIndexing(!params.disableIndexing),
|
|
rego.EvalEarlyExit(!params.disableEarlyExit),
|
|
rego.EvalNondeterministicBuiltins(params.nondeterministicBuiltions),
|
|
}
|
|
|
|
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 len(params.dataPaths.v) > 0 {
|
|
if params.optimizationLevel <= 0 {
|
|
regoArgs = append(regoArgs, rego.Load(params.dataPaths.v, ignored(params.ignore).Apply))
|
|
} else {
|
|
b, err := generateOptimizedBundle(params, false, ignored(params.ignore).Apply, params.dataPaths.v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
regoArgs = append(regoArgs, rego.ParsedBundle("optimized", b))
|
|
}
|
|
}
|
|
|
|
if params.bundlePaths.isFlagSet() {
|
|
if params.optimizationLevel <= 0 {
|
|
for _, bundleDir := range params.bundlePaths.v {
|
|
regoArgs = append(regoArgs, rego.LoadBundle(bundleDir))
|
|
}
|
|
} else {
|
|
b, err := generateOptimizedBundle(params, true, buildCommandLoaderFilter(true, params.ignore), params.bundlePaths.v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
regoArgs = append(regoArgs, rego.ParsedBundle("optimized", b))
|
|
}
|
|
}
|
|
|
|
if params.target.IsSet() {
|
|
regoArgs = append(regoArgs, rego.Target(params.target.String()))
|
|
}
|
|
|
|
inputBytes, err := readInputBytes(params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if inputBytes != nil {
|
|
var input any
|
|
if err := util.Unmarshal(inputBytes, &input); err != nil {
|
|
return nil, fmt.Errorf("unable to parse input: %s", err.Error())
|
|
}
|
|
inputValue, err := ast.InterfaceToValue(input)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to process input: %s", err.Error())
|
|
}
|
|
regoArgs = append(regoArgs, rego.ParsedInput(inputValue))
|
|
}
|
|
|
|
// -s {file} (one input schema file)
|
|
// -s {directory} (one schema directory with input and data schema files)
|
|
schemaSet, err := loader.Schemas(params.schema.path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
regoArgs = append(regoArgs, rego.Schemas(schemaSet))
|
|
|
|
var tracer *topdown.BufferTracer
|
|
|
|
if params.explain != nil && params.explain.String() != explainModeOff {
|
|
tracer = topdown.NewBufferTracer()
|
|
evalArgs = append(evalArgs, rego.EvalQueryTracer(tracer))
|
|
|
|
if params.target.String() == compile.TargetWasm {
|
|
fmt.Fprintf(os.Stderr, "warning: explain mode \"%v\" is not supported with wasm target\n", params.explain.String())
|
|
}
|
|
}
|
|
|
|
var m metrics.Metrics
|
|
if params.metrics {
|
|
m = metrics.New()
|
|
|
|
// Use the same metrics for preparing and evaluating
|
|
regoArgs = append(regoArgs, rego.Metrics(m))
|
|
evalArgs = append(evalArgs, rego.EvalMetrics(m))
|
|
}
|
|
|
|
if params.instrument {
|
|
regoArgs = append(regoArgs, rego.Instrument(true))
|
|
evalArgs = append(evalArgs, rego.EvalInstrument(true))
|
|
}
|
|
|
|
rp := resettableProfiler{}
|
|
if params.profile {
|
|
rp.p = profiler.New()
|
|
evalArgs = append(evalArgs, rego.EvalQueryTracer(&rp))
|
|
}
|
|
|
|
if params.partial {
|
|
regoArgs = append(regoArgs, rego.Unknowns(params.unknowns))
|
|
}
|
|
|
|
regoArgs = append(regoArgs, rego.DisableInlining(params.disableInlining), rego.ShallowInlining(params.shallowInlining))
|
|
|
|
var c *cover.Cover
|
|
|
|
if params.coverage {
|
|
c = cover.New()
|
|
evalArgs = append(evalArgs, rego.EvalQueryTracer(c))
|
|
}
|
|
|
|
if params.strictBuiltinErrors {
|
|
regoArgs = append(regoArgs, rego.StrictBuiltinErrors(true))
|
|
if params.showBuiltinErrors {
|
|
return nil, errors.New("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.C != nil {
|
|
regoArgs = append(regoArgs, rego.Capabilities(params.capabilities.C))
|
|
} else {
|
|
regoArgs = append(regoArgs, rego.Capabilities(ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(params.regoVersion()))))
|
|
}
|
|
|
|
if params.strict {
|
|
regoArgs = append(regoArgs, rego.Strict(params.strict))
|
|
}
|
|
|
|
evalCtx := &evalContext{
|
|
params: params,
|
|
metrics: m,
|
|
profiler: &rp,
|
|
cover: c,
|
|
tracer: tracer,
|
|
regoArgs: regoArgs,
|
|
evalArgs: evalArgs,
|
|
builtInErrorList: &builtInErrors,
|
|
}
|
|
|
|
return evalCtx, nil
|
|
}
|
|
|
|
type resettableProfiler struct {
|
|
p *profiler.Profiler
|
|
}
|
|
|
|
func (r *resettableProfiler) reset() {
|
|
r.p = profiler.New()
|
|
}
|
|
|
|
func (*resettableProfiler) Enabled() bool { return true }
|
|
func (r *resettableProfiler) TraceEvent(ev topdown.Event) { r.p.TraceEvent(ev) }
|
|
func (r *resettableProfiler) Config() topdown.TraceConfig { return r.p.Config() }
|
|
|
|
func getProfileSortOrder(sortOrder []string) []string {
|
|
// convert the sort order slice to a map for faster lookups
|
|
sortOrderMap := make(map[string]bool, len(sortOrder))
|
|
for _, cr := range sortOrder {
|
|
sortOrderMap[cr] = true
|
|
}
|
|
|
|
// compare the given sort order and the default
|
|
for _, cr := range pr.DefaultProfileSortOrder {
|
|
if _, ok := sortOrderMap[cr]; !ok {
|
|
sortOrder = append(sortOrder, cr)
|
|
}
|
|
}
|
|
return sortOrder
|
|
}
|
|
|
|
func readInputBytes(params evalCommandParams) ([]byte, error) {
|
|
if params.stdinInput {
|
|
return io.ReadAll(os.Stdin)
|
|
} else if params.inputPath != "" {
|
|
path, err := fileurl.Clean(params.inputPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return os.ReadFile(path)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
type repeatedStringFlag struct {
|
|
v []string
|
|
isSet bool
|
|
}
|
|
|
|
func newrepeatedStringFlag(val []string) repeatedStringFlag {
|
|
return repeatedStringFlag{
|
|
v: val,
|
|
isSet: false,
|
|
}
|
|
}
|
|
|
|
func (*repeatedStringFlag) Type() string {
|
|
return stringType
|
|
}
|
|
|
|
func (f *repeatedStringFlag) String() string {
|
|
return strings.Join(f.v, ",")
|
|
}
|
|
|
|
func (f *repeatedStringFlag) Set(s string) error {
|
|
f.v = append(f.v, s)
|
|
f.isSet = true
|
|
return nil
|
|
}
|
|
|
|
func (f *repeatedStringFlag) isFlagSet() bool {
|
|
return f.isSet
|
|
}
|
|
|
|
type intFlag struct {
|
|
v int
|
|
isSet bool
|
|
}
|
|
|
|
func newIntFlag(val int) intFlag {
|
|
return intFlag{
|
|
v: val,
|
|
isSet: false,
|
|
}
|
|
}
|
|
|
|
func (*intFlag) Type() string {
|
|
return "int"
|
|
}
|
|
|
|
func (f *intFlag) String() string {
|
|
return strconv.Itoa(f.v)
|
|
}
|
|
|
|
func (f *intFlag) Set(s string) error {
|
|
v, err := strconv.ParseInt(s, 0, 32)
|
|
f.v = int(v)
|
|
f.isSet = true
|
|
return err
|
|
}
|
|
|
|
func (f *intFlag) isFlagSet() bool {
|
|
return f.isSet
|
|
}
|
|
|
|
// resetExprLocations overwrites the row in the location info for every expression contained in pq.
|
|
// The location on every expression is shallow copied to avoid mutating shared state. Overwriting
|
|
// the rows ensures that the formatting package does not leave blank lines in between expressions (e.g.,
|
|
// if expression 1 was saved on L10 and expression 2 was saved on L20 then the formatting package would
|
|
// squash the blank lines.)
|
|
func resetExprLocations(pq *rego.PartialQueries) {
|
|
if pq == nil {
|
|
return
|
|
}
|
|
|
|
vis := &astLocationResetVisitor{}
|
|
|
|
for i := range pq.Queries {
|
|
ast.NewGenericVisitor(vis.visit).Walk(pq.Queries[i])
|
|
}
|
|
|
|
for i := range pq.Support {
|
|
ast.NewGenericVisitor(vis.visit).Walk(pq.Support[i])
|
|
}
|
|
}
|
|
|
|
type astLocationResetVisitor struct {
|
|
n int
|
|
}
|
|
|
|
func (vis *astLocationResetVisitor) visit(x any) bool {
|
|
if expr, ok := x.(*ast.Expr); ok {
|
|
if expr.Location != nil {
|
|
cpy := *expr.Location
|
|
cpy.Row = vis.n
|
|
expr.Location = &cpy
|
|
} else {
|
|
expr.Location = location.NewLocation(nil, "", vis.n, 1)
|
|
}
|
|
vis.n++
|
|
}
|
|
return false
|
|
}
|
|
|
|
func generateOptimizedBundle(params evalCommandParams, asBundle bool, filter loader.Filter, paths []string) (*bundle.Bundle, error) {
|
|
capabilities := params.capabilities.C
|
|
if capabilities == nil {
|
|
capabilities = ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(params.regoVersion()))
|
|
}
|
|
|
|
compiler := compile.New().
|
|
WithCapabilities(capabilities).
|
|
WithTarget(params.target.String()).
|
|
WithAsBundle(asBundle).
|
|
WithBundleLazyLoadingMode(bundle.HasExtension()).
|
|
WithOptimizationLevel(params.optimizationLevel).
|
|
WithOutput(bytes.NewBuffer(nil)).
|
|
WithEntrypoints(params.entrypoints.v...).
|
|
WithRegoAnnotationEntrypoints(true).
|
|
WithPaths(paths...).
|
|
WithFilter(filter).
|
|
WithRegoVersion(params.regoVersion())
|
|
|
|
if err := compiler.Build(context.Background()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return compiler.Bundle(), nil
|
|
}
|