From 30a06544f26b2dd52df8b785543ccc1ba431b54f Mon Sep 17 00:00:00 2001 From: Stephan Renatus Date: Fri, 12 Feb 2021 13:29:02 +0100 Subject: [PATCH] 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 --- .github/workflows/pull-request.yaml | 4 ++ ast/capabilities.go | 7 +++- build/gen-run-go.sh | 2 +- capabilities.json | 3 ++ compile/compile.go | 25 +++++++++--- compile/compile_test.go | 38 +++++++++++++++++-- docs/content/wasm.md | 36 ++++++++++++++---- internal/compiler/wasm/wasm.go | 32 ++++++++++++++++ .../wasm/sdk/opa/capabilities/capabilities.go | 14 +++++++ .../opa/capabilities/capabilities_nowasm.go | 13 +++++++ 10 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 internal/wasm/sdk/opa/capabilities/capabilities.go create mode 100644 internal/wasm/sdk/opa/capabilities/capabilities_nowasm.go 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 +}