wasm: introduce abi_versions to capabilities (#3142)

* wasm: emit ABI version as global

This takes inspiration from the proxy-spec (Envoy's Wasm support).
There, it's recorded in an exported function's name. However, it's
been included like that in the spec because it's the least common
denominator among the different languages (potentially) used to
implement proxy-spec. We've got a pretty good grip on our generated
Wasm code, so we do what's noted in proxy-spec as "ideally, we'd do
xyz instead".

However, our ABI version is a simple integer, no semver.

Ref: https://github.com/proxy-wasm/spec/tree/master/abi-versions/vNEXT#proxy_abi_version_x_y_z

* ast.CapabilitiesForThisVersion: include WasmABIVersions

Extending the ast.Capabilities like this is somewhat unsatisfying -- the Wasm ABI has little to do with the ast package. However, moving Capabilities outside of ast in a way that's not introducing import cycles and is backwards-compatible proved to be quite an effort; so let's go with "simple" here.

* capatibilities.json: ensure it is generated with ABI versions

The build tag `generate` is what `go generate` would set, too. We're losing
that in the main.go -> gen-run-go.sh indirection, so we've got to set it
ourselves.

* ci: fix npm-opa-wasm e2e test

The CI build uses a version of OPA built in a previous step -- with the Wasm SDK _disabled_.
To still build Wasm modules, we thus fix the call to use the capabilities.json file from master,
which corresponds to the capabilities of a build of OPA with Wasm SDK enabled.

* docs/content/wasm.md: mention abi version, change headers

There is only one `#` header in a markdown document, so this fixes
that by adding a few `#`. I haven't added it everywhere below
`# Compiling`, but I think the structure is OK now.

Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
This commit is contained in:
Stephan Renatus
2021-02-12 13:29:02 +01:00
committed by GitHub
parent 58be4ee36b
commit 30a06544f2
10 changed files with 153 additions and 21 deletions
+4
View File
@@ -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
+5 -2
View File
@@ -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 {
+1 -1
View File
@@ -2,4 +2,4 @@
set -e
GOFLAGS=-mod=vendor GO111MODULE=on GOOS="" GOARCH="" go run $@
GOFLAGS=-mod=vendor GO111MODULE=on GOOS="" GOARCH="" go run -tags generate $@
+3
View File
@@ -3379,5 +3379,8 @@
"type": "function"
}
}
],
"wasm_abi_versions": [
1
]
}
+19 -6
View File
@@ -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
}
+34 -4
View File
@@ -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)
+28 -8
View File
@@ -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 <opa_wasm_abi_version> - 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
+32
View File
@@ -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
@@ -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[:]
}
@@ -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
}