rego: make wasmtime-go dependency "more optional" (#3708)

Users of OPA as a library are concerned about big binary blobs in their vendor/
directories. Even more so if they don't use them. This is the case for anyone
using OPA as library, but not using the wasm-backed evaluation feature.

With this change, importers of any packages other than `server` and `cmd`
will have to explicitly opt-in to using wasm evaluation features by having an
underscore import somewhere:

    import _ "github.com/open-policy-agent/opa/features/wasm"

Fixes #3545.

Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
This commit is contained in:
Stephan Renatus
2021-08-16 11:48:43 +02:00
committed by GitHub
parent ad8a937a39
commit 6e4377af59
18 changed files with 246 additions and 192 deletions
+9
View File
@@ -0,0 +1,9 @@
// Copyright 2021 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.
// +build opa_wasm
package cmd
import _ "github.com/open-policy-agent/opa/features/wasm"
+1 -1
View File
@@ -45,7 +45,7 @@ func generateCmdOutput(out io.Writer, check bool) {
var wasmAvailable string
if version.WasmRuntimeAvailable {
if version.WasmRuntimeAvailable() {
wasmAvailable = "available"
} else {
wasmAvailable = "unavailable"
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2021 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.
// Import this package to enable evaluation of rego code using the
// built-in wasm engine.
package wasm
import (
"context"
"github.com/open-policy-agent/opa/internal/rego/opa"
wopa "github.com/open-policy-agent/opa/internal/wasm/sdk/opa"
)
func init() {
opa.RegisterEngine("wasm", &factory{})
}
// OPA is an implementation of the OPA SDK.
type OPA struct {
opa *wopa.OPA
}
type factory struct{}
// New constructs a new OPA instance.
func (*factory) New() opa.EvalEngine {
return &OPA{opa: wopa.New()}
}
// WithPolicyBytes configures the compiled policy to load.
func (o *OPA) WithPolicyBytes(policy []byte) opa.EvalEngine {
o.opa = o.opa.WithPolicyBytes(policy)
return o
}
// WithDataJSON configures the JSON data to load.
func (o *OPA) WithDataJSON(data interface{}) opa.EvalEngine {
o.opa = o.opa.WithDataJSON(data)
return o
}
// Init initializes the OPA instance.
func (o *OPA) Init() (opa.EvalEngine, error) {
i, err := o.opa.Init()
if err != nil {
return nil, err
}
o.opa = i
return o, nil
}
func (o *OPA) Entrypoints(ctx context.Context) (map[string]int32, error) {
return o.opa.Entrypoints(ctx)
}
// Eval evaluates the policy.
func (o *OPA) Eval(ctx context.Context, opts opa.EvalOpts) (*opa.Result, error) {
evalOptions := wopa.EvalOpts{
Input: opts.Input,
Metrics: opts.Metrics,
Entrypoint: opts.Entrypoint,
Time: opts.Time,
Seed: opts.Seed,
InterQueryBuiltinCache: opts.InterQueryBuiltinCache,
}
res, err := o.opa.Eval(ctx, evalOptions)
if err != nil {
return nil, err
}
return &opa.Result{Result: res.Result}, nil
}
func (o *OPA) SetData(ctx context.Context, data interface{}) error {
return o.opa.SetData(ctx, data)
}
func (o *OPA) SetDataPath(ctx context.Context, path []string, data interface{}) error {
return o.opa.SetDataPath(ctx, path, data)
}
func (o *OPA) RemoveDataPath(ctx context.Context, path []string) error {
return o.opa.RemoveDataPath(ctx, path)
}
func (o *OPA) Close() {
o.opa.Close()
}
+5 -2
View File
@@ -198,6 +198,9 @@ func NewOutputErrors(err error) []OutputError {
Message: err.Error(),
err: typedErr,
}}
if d, ok := err.(rego.ErrorWithDetails); ok {
errs[0].Details = d.Details()
}
}
}
return errs
@@ -388,8 +391,8 @@ func Raw(w io.Writer, r Output) error {
return nil
}
func prettyError(w io.Writer, err error) error {
_, err = fmt.Fprintln(w, err)
func prettyError(w io.Writer, errs OutputErrors) error {
_, err := fmt.Fprintln(w, errs)
return err
}
@@ -36,6 +36,15 @@ func (t *testErrorWithMarshaller) MarshalJSON() ([]byte, error) {
})
}
type testErrorWithDetails struct{}
func (*testErrorWithDetails) Error() string { return "something went wrong" }
func (*testErrorWithDetails) Details() string {
return `oh
so
wrong`
}
func validateJSONOutput(t *testing.T, testErr error, expected string) {
t.Helper()
output := Output{Errors: NewOutputErrors(testErr)}
@@ -81,6 +90,21 @@ func TestOutputJSONErrorCustomMarshaller(t *testing.T) {
validateJSONOutput(t, err, expected)
}
func TestOutputJSONErrorWithDetails(t *testing.T) {
err := &testErrorWithDetails{}
expected := `{
"errors": [
{
"message": "something went wrong",
"details": "oh\nso\nwrong"
}
]
}
`
validateJSONOutput(t, err, expected)
}
func TestOutputJSONErrorStructuredASTErr(t *testing.T) {
err := &ast.Error{
Code: "1",
@@ -454,6 +478,17 @@ func TestRaw(t *testing.T) {
},
want: "1 error occurred: boom\n",
},
{
// NOTE(sr): The presentation package outputs whatever Error() on
// the errors it is given yields. So even though NewOutputErrors
// will pick up the error details, they won't be output, as they
// are not included in the error's Error() string.
note: "error with details",
output: Output{
Errors: NewOutputErrors(&testErrorWithDetails{}),
},
want: "1 error occurred: something went wrong\n",
},
}
for _, tc := range tests {
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2021 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 opa
import (
"context"
)
// ErrEngineNotFound is returned by LookupEngine if no wasm engine was
// registered by that name.
var ErrEngineNotFound error = &errEngineNotFound{}
type errEngineNotFound struct{}
func (*errEngineNotFound) Error() string { return "engine not found" }
func (*errEngineNotFound) Details() string {
return `WebAssembly runtime not supported in this build.
----------------------------------------------------------------------------------
Please download an OPA binary with Wasm enabled from
https://www.openpolicyagent.org/docs/latest/#running-opa
or build it yourself (with Wasm enabled).
----------------------------------------------------------------------------------
`
}
// Engine repesents a factory for instances of EvalEngine implementations
type Engine interface {
New() EvalEngine
}
// EvalEngine is the interface implemented by an engine used to eval a policy
type EvalEngine interface {
Init() (EvalEngine, error)
Entrypoints(context.Context) (map[string]int32, error)
WithPolicyBytes([]byte) EvalEngine
WithDataJSON(interface{}) EvalEngine
Eval(context.Context, EvalOpts) (*Result, error)
SetData(context.Context, interface{}) error
SetDataPath(context.Context, []string, interface{}) error
RemoveDataPath(context.Context, []string) error
Close()
}
var engines = map[string]Engine{}
// RegisterEngine registers an evaluation engine by its target name.
// Note that the "rego" target is always available.
func RegisterEngine(name string, e Engine) {
if engines[name] != nil {
panic("duplicate engine registration")
}
engines[name] = e
}
// LookupEngine allows retrieving an engine registered by name
func LookupEngine(name string) (Engine, error) {
e, ok := engines[name]
if !ok {
return nil, ErrEngineNotFound
}
return e, nil
}
-50
View File
@@ -1,50 +0,0 @@
// Copyright 2021 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.
// +build !opa_wasm
package opa
import (
"context"
"fmt"
"os"
)
// OPA is a stub implementation of a opa.OPA.
type OPA struct {
}
// New unimplemented.
func New() *OPA {
fmt.Fprintf(os.Stderr, `WebAssembly runtime not supported in this build.
----------------------------------------------------------------------------------
Please download an OPA binary with Wasm enabled from
https://www.openpolicyagent.org/docs/latest/#running-opa
or build it yourself (with Wasm enabled).
----------------------------------------------------------------------------------
`)
os.Exit(1)
return nil
}
// WithPolicyBytes unimplemented.
func (o *OPA) WithPolicyBytes(policy []byte) *OPA {
panic("unreachable")
}
// WithDataJSON unimplemented.
func (o *OPA) WithDataJSON(data interface{}) *OPA {
panic("unreachable")
}
// Init unimplemented.
func (o *OPA) Init() (*OPA, error) {
panic("unreachable")
}
// Eval unimplemented.
func (o *OPA) Eval(ctx context.Context, opts EvalOpts) (*Result, error) {
panic("unreachable")
}
-63
View File
@@ -1,63 +0,0 @@
// Copyright 2021 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.
// +build opa_wasm
package opa
import (
"context"
wopa "github.com/open-policy-agent/opa/internal/wasm/sdk/opa"
)
// OPA is an implementation of the OPA SDK.
type OPA struct {
opa *wopa.OPA
}
// New constructs a new OPA instance.
func New() *OPA {
return &OPA{opa: wopa.New()}
}
// WithPolicyBytes configures the compiled policy to load.
func (o *OPA) WithPolicyBytes(policy []byte) *OPA {
o.opa = o.opa.WithPolicyBytes(policy)
return o
}
// WithDataJSON configures the JSON data to load.
func (o *OPA) WithDataJSON(data interface{}) *OPA {
o.opa = o.opa.WithDataJSON(data)
return o
}
// Init initializes the OPA instance.
func (o *OPA) Init() (*OPA, error) {
i, err := o.opa.Init()
if err != nil {
return nil, err
}
o.opa = i
return o, nil
}
// Eval evaluates the policy.
func (o *OPA) Eval(ctx context.Context, opts EvalOpts) (*Result, error) {
evalOptions := wopa.EvalOpts{
Input: opts.Input,
Metrics: opts.Metrics,
Time: opts.Time,
Seed: opts.Seed,
InterQueryBuiltinCache: opts.InterQueryBuiltinCache,
}
res, err := o.opa.Eval(ctx, evalOptions)
if err != nil {
return nil, err
}
return &Result{Result: res.Result}, nil
}
+1
View File
@@ -17,6 +17,7 @@ type Result struct {
type EvalOpts struct {
Input *interface{}
Metrics metrics.Metrics
Entrypoint int32
Time time.Time
Seed io.Reader
InterQueryBuiltinCache cache.InterQueryCache
+6
View File
@@ -16,3 +16,9 @@ func (h *HaltError) Error() string {
func NewHaltError(err error) error {
return &HaltError{err: err}
}
// ErrorWithDetails interface is satisfied by an error that provides further
// details.
type ErrorWithDetails interface {
Details() string
}
+7 -2
View File
@@ -550,7 +550,7 @@ type Rego struct {
resolvers []refResolver
schemaSet *ast.SchemaSet
target string // target type (wasm, rego, etc.)
opa *opa.OPA
opa opa.EvalEngine
}
// Function represents a built-in function that is callable in Rego.
@@ -1501,7 +1501,12 @@ func (r *Rego) PrepareForEval(ctx context.Context, opts ...PrepareOption) (Prepa
return PreparedEvalQuery{}, err
}
o, err := opa.New().WithPolicyBytes(cr.Bytes).WithDataJSON(data).Init()
e, err := opa.LookupEngine(targetWasm)
if err != nil {
return PreparedEvalQuery{}, err
}
o, err := e.New().WithPolicyBytes(cr.Bytes).WithDataJSON(data).Init()
if err != nil {
_ = txnClose(ctx, err) // Ignore error
return PreparedEvalQuery{}, err
+3
View File
@@ -16,11 +16,14 @@ import (
"time"
"github.com/fortytw2/leaktest"
"github.com/open-policy-agent/opa/ast"
sdk_errors "github.com/open-policy-agent/opa/internal/wasm/sdk/opa/errors"
"github.com/open-policy-agent/opa/storage/inmem"
"github.com/open-policy-agent/opa/topdown"
"github.com/open-policy-agent/opa/topdown/cache"
_ "github.com/open-policy-agent/opa/features/wasm"
)
func TestPrepareAndEvalWithWasmTarget(t *testing.T) {
+2
View File
@@ -10,6 +10,8 @@ import (
"bytes"
"context"
"testing"
_ "github.com/open-policy-agent/opa/features/wasm"
)
func TestReplWasmTarget(t *testing.T) {
-55
View File
@@ -1,55 +0,0 @@
// Copyright 2020 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.
// +build !opa_wasm
package wasm
import (
"context"
"errors"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/resolver"
)
// Resolver is a stub implementation of a resolver.Resolver.
type Resolver struct {
}
// Entrypoints unimplemented.
func (r *Resolver) Entrypoints() []ast.Ref {
panic("unreachable")
}
// Close unimplemented.
func (r *Resolver) Close() {
panic("unreachable")
}
// Eval unimplemented.
func (r *Resolver) Eval(context.Context, resolver.Input) (resolver.Result, error) {
panic("unreachable")
}
// SetData unimplemented.
func (r *Resolver) SetData(context.Context, interface{}) error {
panic("unreachable")
}
// SetDataPath unimplemented.
func (r *Resolver) SetDataPath(context.Context, []string, interface{}) error {
panic("unreachable")
}
// RemoveDataPath unimplemented.
func (r *Resolver) RemoveDataPath(context.Context, []string) error {
panic("unreachable")
}
// New unimplemented. Will always return an error.
func New([]ast.Ref, []byte, interface{}) (*Resolver, error) {
return nil, errors.New("WebAssembly runtime not supported in this build")
}
+7 -6
View File
@@ -2,8 +2,6 @@
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// +build opa_wasm
package wasm
import (
@@ -11,16 +9,19 @@ import (
"fmt"
"strconv"
"github.com/open-policy-agent/opa/internal/wasm/sdk/opa"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/internal/rego/opa"
"github.com/open-policy-agent/opa/resolver"
)
// New creates a new Resolver instance which is using the Wasm module
// policy for the given entrypoint ref.
func New(entrypoints []ast.Ref, policy []byte, data interface{}) (*Resolver, error) {
o, err := opa.New().
e, err := opa.LookupEngine("wasm")
if err != nil {
return nil, err
}
o, err := e.New().
WithPolicyBytes(policy).
WithDataJSON(data).
Init()
@@ -63,7 +64,7 @@ func New(entrypoints []ast.Ref, policy []byte, data interface{}) (*Resolver, err
type Resolver struct {
entrypoints []ast.Ref
entrypointIDs *ast.ValueMap
o *opa.OPA
o opa.EvalEngine
}
// Entrypoints returns a list of entrypoints this resolver is configured to
+9
View File
@@ -0,0 +1,9 @@
// Copyright 2021 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.
// +build opa_wasm
package server
import _ "github.com/open-policy-agent/opa/features/wasm"
+6 -3
View File
@@ -2,9 +2,12 @@
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// +build opa_wasm
package version
import "github.com/open-policy-agent/opa/internal/rego/opa"
// WasmRuntimeAvailable indicates if a wasm runtime is available in this OPA.
const WasmRuntimeAvailable = true
func WasmRuntimeAvailable() bool {
_, err := opa.LookupEngine("wasm")
return err == nil
}
-10
View File
@@ -1,10 +0,0 @@
// Copyright 2020 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.
// +build !opa_wasm
package version
// WasmRuntimeAvailable indicates if a wasm runtime is available in this OPA.
const WasmRuntimeAvailable = false