From 6e4377af592677f68f4041e185ebe8e252ecb879 Mon Sep 17 00:00:00 2001 From: Stephan Renatus Date: Mon, 16 Aug 2021 11:48:43 +0200 Subject: [PATCH] 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 --- cmd/features.go | 9 +++ cmd/version.go | 2 +- features/wasm/wasm.go | 91 ++++++++++++++++++++++ internal/presentation/presentation.go | 7 +- internal/presentation/presentation_test.go | 35 +++++++++ internal/rego/opa/engine.go | 64 +++++++++++++++ internal/rego/opa/nop.go | 50 ------------ internal/rego/opa/opa.go | 63 --------------- internal/rego/opa/options.go | 1 + rego/errors.go | 6 ++ rego/rego.go | 9 ++- rego/rego_wasmtarget_test.go | 3 + repl/repl_wasmtarget_test.go | 2 + resolver/wasm/nop.go | 55 ------------- resolver/wasm/wasm.go | 13 ++-- server/features.go | 9 +++ version/wasm.go | 9 ++- version/wasm_nop.go | 10 --- 18 files changed, 246 insertions(+), 192 deletions(-) create mode 100644 cmd/features.go create mode 100644 features/wasm/wasm.go create mode 100644 internal/rego/opa/engine.go delete mode 100644 internal/rego/opa/nop.go delete mode 100644 internal/rego/opa/opa.go delete mode 100644 resolver/wasm/nop.go create mode 100644 server/features.go delete mode 100644 version/wasm_nop.go diff --git a/cmd/features.go b/cmd/features.go new file mode 100644 index 0000000000..464f4005cc --- /dev/null +++ b/cmd/features.go @@ -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" diff --git a/cmd/version.go b/cmd/version.go index 8c8118b272..3ecb5e22dc 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -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" diff --git a/features/wasm/wasm.go b/features/wasm/wasm.go new file mode 100644 index 0000000000..009768ed28 --- /dev/null +++ b/features/wasm/wasm.go @@ -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() +} diff --git a/internal/presentation/presentation.go b/internal/presentation/presentation.go index e2b52a36a3..2f3536917f 100644 --- a/internal/presentation/presentation.go +++ b/internal/presentation/presentation.go @@ -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 } diff --git a/internal/presentation/presentation_test.go b/internal/presentation/presentation_test.go index dc7b76f8d5..686a59a66c 100644 --- a/internal/presentation/presentation_test.go +++ b/internal/presentation/presentation_test.go @@ -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 { diff --git a/internal/rego/opa/engine.go b/internal/rego/opa/engine.go new file mode 100644 index 0000000000..872f6e1793 --- /dev/null +++ b/internal/rego/opa/engine.go @@ -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 +} diff --git a/internal/rego/opa/nop.go b/internal/rego/opa/nop.go deleted file mode 100644 index 4241a83095..0000000000 --- a/internal/rego/opa/nop.go +++ /dev/null @@ -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") -} diff --git a/internal/rego/opa/opa.go b/internal/rego/opa/opa.go deleted file mode 100644 index c5bdd06e50..0000000000 --- a/internal/rego/opa/opa.go +++ /dev/null @@ -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 -} diff --git a/internal/rego/opa/options.go b/internal/rego/opa/options.go index f0ac32c607..e74a45fec3 100644 --- a/internal/rego/opa/options.go +++ b/internal/rego/opa/options.go @@ -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 diff --git a/rego/errors.go b/rego/errors.go index 0f6d73989d..8e83cb5706 100644 --- a/rego/errors.go +++ b/rego/errors.go @@ -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 +} diff --git a/rego/rego.go b/rego/rego.go index d8881d3fa7..2cfbf0808c 100644 --- a/rego/rego.go +++ b/rego/rego.go @@ -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 diff --git a/rego/rego_wasmtarget_test.go b/rego/rego_wasmtarget_test.go index 545892bfa0..787fe4fbc4 100644 --- a/rego/rego_wasmtarget_test.go +++ b/rego/rego_wasmtarget_test.go @@ -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) { diff --git a/repl/repl_wasmtarget_test.go b/repl/repl_wasmtarget_test.go index fe6b3ef30f..a7397d9332 100644 --- a/repl/repl_wasmtarget_test.go +++ b/repl/repl_wasmtarget_test.go @@ -10,6 +10,8 @@ import ( "bytes" "context" "testing" + + _ "github.com/open-policy-agent/opa/features/wasm" ) func TestReplWasmTarget(t *testing.T) { diff --git a/resolver/wasm/nop.go b/resolver/wasm/nop.go deleted file mode 100644 index 9fe98cb851..0000000000 --- a/resolver/wasm/nop.go +++ /dev/null @@ -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") -} diff --git a/resolver/wasm/wasm.go b/resolver/wasm/wasm.go index 1b3dd0c52f..9c13879dc3 100644 --- a/resolver/wasm/wasm.go +++ b/resolver/wasm/wasm.go @@ -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 diff --git a/server/features.go b/server/features.go new file mode 100644 index 0000000000..37de967a71 --- /dev/null +++ b/server/features.go @@ -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" diff --git a/version/wasm.go b/version/wasm.go index d33b4770c4..c274a7827a 100644 --- a/version/wasm.go +++ b/version/wasm.go @@ -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 +} diff --git a/version/wasm_nop.go b/version/wasm_nop.go deleted file mode 100644 index 7668f06f03..0000000000 --- a/version/wasm_nop.go +++ /dev/null @@ -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