Skip to content

Commit

Permalink
rego: make wasmtime-go dependency "more optional" (open-policy-agent#…
Browse files Browse the repository at this point in the history
…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 open-policy-agent#3545.

Signed-off-by: Stephan Renatus <[email protected]>
  • Loading branch information
srenatus authored and jgchn committed Aug 20, 2021
1 parent b70a4e5 commit 8cac601
Show file tree
Hide file tree
Showing 18 changed files with 246 additions and 192 deletions.
9 changes: 9 additions & 0 deletions cmd/features.go
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
91 changes: 91 additions & 0 deletions features/wasm/wasm.go
Original file line number Diff line number Diff line change
@@ -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()
}
7 changes: 5 additions & 2 deletions internal/presentation/presentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
35 changes: 35 additions & 0 deletions internal/presentation/presentation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down
64 changes: 64 additions & 0 deletions internal/rego/opa/engine.go
Original file line number Diff line number Diff line change
@@ -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 changes: 0 additions & 50 deletions internal/rego/opa/nop.go

This file was deleted.

63 changes: 0 additions & 63 deletions internal/rego/opa/opa.go

This file was deleted.

1 change: 1 addition & 0 deletions internal/rego/opa/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions rego/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading

0 comments on commit 8cac601

Please sign in to comment.