diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index fcdd93a6b4..df07a623fa 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -102,8 +102,12 @@ jobs: repository: open-policy-agent/npm-opa-wasm path: npm-opa-wasm + # NOTE(sr): the opa binary built above doesn't have wasm enabled, + # so we override the capabilities for what the released binary + # provides. - name: Run npm-opa-wasm nodejs-app examples run: | npm install + sed -i 's/opa build/opa build --capabilities=..\/..\/..\/capabilities.json/' examples/nodejs-ts-app/package.json ./e2e.sh working-directory: npm-opa-wasm diff --git a/ast/capabilities.go b/ast/capabilities.go index 50765b616d..ff25c427b3 100644 --- a/ast/capabilities.go +++ b/ast/capabilities.go @@ -8,20 +8,23 @@ import ( "io" "sort" + "github.com/open-policy-agent/opa/internal/wasm/sdk/opa/capabilities" "github.com/open-policy-agent/opa/util" ) // Capabilities defines a structure containing data that describes the capablilities // or features supported by a particular version of OPA. type Capabilities struct { - Builtins []*Builtin `json:"builtins"` // builtins is a set of built-in functions that are supported. + Builtins []*Builtin `json:"builtins"` // builtins is a set of built-in functions that are supported. + WasmABIVersions []int `json:"wasm_abi_versions"` } // CapabilitiesForThisVersion returns the capabilities of this version of OPA. func CapabilitiesForThisVersion() *Capabilities { f := &Capabilities{ - Builtins: []*Builtin{}, + Builtins: []*Builtin{}, + WasmABIVersions: capabilities.ABIVersions(), } for _, bi := range Builtins { diff --git a/build/gen-run-go.sh b/build/gen-run-go.sh index 226b30ed4f..60e24135f5 100755 --- a/build/gen-run-go.sh +++ b/build/gen-run-go.sh @@ -2,4 +2,4 @@ set -e -GOFLAGS=-mod=vendor GO111MODULE=on GOOS="" GOARCH="" go run $@ \ No newline at end of file +GOFLAGS=-mod=vendor GO111MODULE=on GOOS="" GOARCH="" go run -tags generate $@ \ No newline at end of file diff --git a/capabilities.json b/capabilities.json index 6910546b42..e3b3afca09 100644 --- a/capabilities.json +++ b/capabilities.json @@ -3379,5 +3379,8 @@ "type": "function" } } + ], + "wasm_abi_versions": [ + 1 ] } diff --git a/compile/compile.go b/compile/compile.go index 21490cf29f..de61ba1e1b 100644 --- a/compile/compile.go +++ b/compile/compile.go @@ -14,8 +14,6 @@ import ( "sort" "strings" - "github.com/open-policy-agent/opa/internal/wasm/encoding" - "github.com/pkg/errors" "github.com/open-policy-agent/opa/ast" @@ -24,6 +22,7 @@ import ( "github.com/open-policy-agent/opa/internal/planner" "github.com/open-policy-agent/opa/internal/ref" initload "github.com/open-policy-agent/opa/internal/runtime/init" + "github.com/open-policy-agent/opa/internal/wasm/encoding" "github.com/open-policy-agent/opa/loader" "github.com/open-policy-agent/opa/rego" "github.com/open-policy-agent/opa/storage" @@ -44,8 +43,8 @@ const ( const wasmResultVar = ast.Var("result") var validTargets = map[string]struct{}{ - TargetRego: struct{}{}, - TargetWasm: struct{}{}, + TargetRego: {}, + TargetWasm: {}, } // Compiler implements bundle compilation and linking. @@ -431,6 +430,21 @@ func (c *Compiler) compileWasm(ctx context.Context) error { builtins[bi.Name] = bi } + compiler := wasm.New() + found := false + for _, v := range c.capabilities.WasmABIVersions { + if v == compiler.ABIVersion() { + found = true + break + } + } + if !found { + return fmt.Errorf("compiler ABI version not in capabilities (have %v, want %d)", + c.capabilities.WasmABIVersions, + compiler.ABIVersion(), + ) + } + // Plan the query sets. p := planner.New(). WithQueries(queries). @@ -445,13 +459,12 @@ func (c *Compiler) compileWasm(ctx context.Context) error { } // Compile the policy into a wasm binary. - m, err := wasm.New().WithPolicy(policy).Compile() + m, err := compiler.WithPolicy(policy).Compile() if err != nil { return err } var buf bytes.Buffer - if err := encoding.WriteModule(&buf, m); err != nil { return err } diff --git a/compile/compile_test.go b/compile/compile_test.go index e09abd52c3..ac2d7a7061 100644 --- a/compile/compile_test.go +++ b/compile/compile_test.go @@ -427,6 +427,13 @@ func TestCompilerOptimizationL2(t *testing.T) { }) } +// NOTE(sr): we override this to not depend on build tags in tests +func wasmABIVersions(vs ...int) *ast.Capabilities { + caps := ast.CapabilitiesForThisVersion() + caps.WasmABIVersions = vs + return caps +} + func TestCompilerWasmTarget(t *testing.T) { files := map[string]string{ "test.rego": `package test @@ -437,7 +444,8 @@ func TestCompilerWasmTarget(t *testing.T) { test.WithTempFS(files, func(root string) { - compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/p", "test/q") + compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/p", "test/q"). + WithCapabilities(wasmABIVersions(1)) err := compiler.Build(context.Background()) if err != nil { t.Fatal(err) @@ -455,6 +463,25 @@ func TestCompilerWasmTarget(t *testing.T) { }) } +func TestCompilerWasmTargetWithCapabilitiesMismatch(t *testing.T) { + files := map[string]string{ + "test.rego": `package test + + p = 7 + q = p+1`, + } + + test.WithTempFS(files, func(root string) { + + compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/p", "test/q"). + WithCapabilities(wasmABIVersions(0, 2)) + err := compiler.Build(context.Background()) + if err == nil { + t.Fatal("expected err, got nil") + } + }) +} + func TestCompilerWasmTargetMultipleEntrypoints(t *testing.T) { files := map[string]string{ "test.rego": `package test @@ -470,7 +497,8 @@ func TestCompilerWasmTargetMultipleEntrypoints(t *testing.T) { test.WithTempFS(files, func(root string) { - compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/p", "policy/authz") + compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/p", "policy/authz"). + WithCapabilities(wasmABIVersions(1)) err := compiler.Build(context.Background()) if err != nil { t.Fatal(err) @@ -514,7 +542,8 @@ func TestCompilerWasmTargetEntrypointDependents(t *testing.T) { test.WithTempFS(files, func(root string) { - compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/r", "test/z") + compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/r", "test/z"). + WithCapabilities(wasmABIVersions(1)) err := compiler.Build(context.Background()) if err != nil { t.Fatal(err) @@ -565,7 +594,8 @@ func TestCompilerWasmTargetLazyCompile(t *testing.T) { test.WithTempFS(files, func(root string) { - compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/p").WithOptimizationLevel(1) + compiler := New().WithPaths(root).WithTarget("wasm").WithEntrypoints("test/p").WithOptimizationLevel(1). + WithCapabilities(wasmABIVersions(1)) err := compiler.Build(context.Background()) if err != nil { t.Fatal(err) diff --git a/docs/content/wasm.md b/docs/content/wasm.md index 760880dc21..f1ba8370ff 100644 --- a/docs/content/wasm.md +++ b/docs/content/wasm.md @@ -13,7 +13,7 @@ As described on [https://webassembly.org/](https://webassembly.org/) > compilation of high-level languages like C/C++/Rust, enabling deployment on > the web for client and server applications. -# Overview +## Overview OPA is able to compile Rego policies into executable Wasm modules that can be evaluated with different inputs and external data. This is *not* running the OPA @@ -27,7 +27,7 @@ functions that are not, and probably won't be natively supported in Wasm (e.g., `http.send`). Built-in functions that are not natively supported can be implemented in the host environment (e.g., JavaScript). -# Compiling Policies +## Compiling Policies You can compile Rego policies into Wasm modules using the `opa build` subcommand. @@ -65,7 +65,7 @@ empty (indicating an undefined policy decision) otherwise they should select the > For more information on `opa build` run `opa build --help`. -## Advanced Compiling Options +### Advanced Compiling Options You can also compile Rego policies into Wasm modules from Go using the lower-level [rego](https://godoc.org/github.com/open-policy-agent/opa/rego#Rego.Compile) API @@ -73,9 +73,9 @@ that produces raw Wasm executables and the higher-level [compile]() API that produces OPA bundle files. The [compile](https://godoc.org/github.com/open-policy-agent/opa/compile#Compiler.Build) API is recommended. -# Using Compiled Policies +## Using Compiled Policies -## JavaScript SDK +### JavaScript SDK There is a JavaScript SDK available that simplifies the process of loading and evaluating compiled policies. If you want to evaluate Rego policies inside @@ -88,12 +88,12 @@ for more details. There is an example NodeJS application located [here](https://github.com/open-policy-agent/npm-opa-wasm/tree/master/examples/nodejs-app). -## From Scratch +### From Scratch If you want to integrate Wasm compiled policies into a language or runtime that does not have SDK support, read this section. -### Instantiating the Wasm Module +#### Instantiating the Wasm Module Before you can evaluate Wasm compiled policies you need to instantiate the Wasm module produced by the compilation process described earlier on this page. @@ -104,6 +104,26 @@ of import functions. The memory buffer is a contiguous, mutable byte-array that allows you to pass data to the policy and receive output from the policy. The import functions are dependencies of the compiled policies. +#### ABI Versions + +Wasm modules built using OPA 0.27.0 onwards contain a global variable named +`opa_wasm_abi_version` that has a constant i32 value indicating the ABI version +this module requires. Described below you find ABI version 1. + +Using tools like `wasm-objdump` (`wasm-objdump -x policy.wasm`), the ABI +version can be found here: + +``` +Global[2]: + - global[0] i32 mutable=1 - init i32=121904 + - global[1] i32 mutable=0 - init i32=1 +Export[19]: +[...] + - global[1] -> "opa_wasm_abi_version" +``` + +Note the `i32=1` of `global[1]`, exported by the name of `opa_wasm_abi_version`. + #### Exports The primary exported functions for interacting with policy modules are: @@ -205,7 +225,7 @@ element: When the evaluation runs, the `opa_builtin1` callback would invoked with `builtin_id` set to `0`. -### Evaluation +#### Evaluation Once instantiated, the policy module is ready to be evaluated. Use the `opa_eval_ctx_new` exported function to create an evaluation context. Use the diff --git a/internal/compiler/wasm/wasm.go b/internal/compiler/wasm/wasm.go index d91cc99ac6..ae8e1ad250 100644 --- a/internal/compiler/wasm/wasm.go +++ b/internal/compiler/wasm/wasm.go @@ -21,6 +21,12 @@ import ( "github.com/open-policy-agent/opa/internal/wasm/types" ) +// Record Wasm ABI version in exported global variable +const ( + opaWasmABIVersionVal = 1 + opaWasmABIVersionVar = "opa_wasm_abi_version" +) + const ( opaTypeNull int32 = iota + 1 opaTypeBoolean @@ -215,6 +221,12 @@ func New() *Compiler { return c } +// ABIVersion returns the Wasm ABI version this compiler +// emits. +func (*Compiler) ABIVersion() int { + return opaWasmABIVersionVal +} + // WithPolicy sets the policy to compile. func (c *Compiler) WithPolicy(p *ir.Policy) *Compiler { c.policy = p @@ -250,6 +262,26 @@ func (c *Compiler) initModule() error { return err } + // add global for ABI version, export it + abiVersionGlobal := module.Global{ + Type: types.I32, + Mutable: false, + Init: module.Expr{ + Instrs: []instruction.Instruction{ + instruction.I32Const{Value: opaWasmABIVersionVal}, + }, + }, + } + abiVersionExport := module.Export{ + Name: opaWasmABIVersionVar, + Descriptor: module.ExportDescriptor{ + Type: module.GlobalExportType, + Index: uint32(len(c.module.Global.Globals)), + }, + } + c.module.Global.Globals = append(c.module.Global.Globals, abiVersionGlobal) + c.module.Export.Exports = append(c.module.Export.Exports, abiVersionExport) + c.funcs = make(map[string]uint32) for _, fn := range c.module.Names.Functions { c.funcs[fn.Name] = fn.Index diff --git a/internal/wasm/sdk/opa/capabilities/capabilities.go b/internal/wasm/sdk/opa/capabilities/capabilities.go new file mode 100644 index 0000000000..6d4adf843b --- /dev/null +++ b/internal/wasm/sdk/opa/capabilities/capabilities.go @@ -0,0 +1,14 @@ +// 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 generate + +package capabilities + +var wasmABIVersions = [...]int{1} + +// ABIVersions returns the ABI versions that this SDK supports +func ABIVersions() []int { + return wasmABIVersions[:] +} diff --git a/internal/wasm/sdk/opa/capabilities/capabilities_nowasm.go b/internal/wasm/sdk/opa/capabilities/capabilities_nowasm.go new file mode 100644 index 0000000000..94ecefb099 --- /dev/null +++ b/internal/wasm/sdk/opa/capabilities/capabilities_nowasm.go @@ -0,0 +1,13 @@ +// 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,!generate + +package capabilities + +// ABIVersions returns the supported Wasm ABI versions for this +// build: none +func ABIVersions() []int { + return nil +}