Skip to content

Commit

Permalink
wasm: Add parameters handling to wasm
Browse files Browse the repository at this point in the history
Allow gadgets developers to interact with parameters
from the wasm module:
- get value of parameters

Signed-off-by: Mauricio Vásquez <mauriciov@microsoft.com>
  • Loading branch information
mauriciovasquezbernal committed Sep 17, 2024
1 parent b06986c commit 0f85578
Show file tree
Hide file tree
Showing 16 changed files with 295 additions and 8 deletions.
30 changes: 30 additions & 0 deletions docs/gadget-devel/gadget-wasm-api-raw.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,33 @@ Parameters:

Return value:
- None

### Parameters

Parameters passed to the WASM module are defined in the metadata file as this:

```yaml
...
params:
wasm:
param-key:
key: param-key
description: param-description
defaultValue: param-default-value
typeHint: param-type-hint
title: param-title
alias: param-alias
isMandatory: true
param-key2:
...
```
#### `getParamValue(key string) string`

Return the value of a parameter.

Parameters:
- `key` (string): Key of the parameter.

Return value:
- The value of the parameter.
5 changes: 5 additions & 0 deletions pkg/operators/wasm/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ func bufFromStack(m wapi.Module, val uint64) ([]byte, error) {
}

func stringFromStack(m wapi.Module, val uint64) (string, error) {
// handle empty strings in a special way
if val == 0 {
return "", nil
}

buf, err := bufFromStack(m, val)
if err != nil {
return "", err
Expand Down
64 changes: 64 additions & 0 deletions pkg/operators/wasm/params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2024 The Inspektor Gadget authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package wasm

import (
"context"

"github.com/tetratelabs/wazero"
wapi "github.com/tetratelabs/wazero/api"
)

func (i *wasmOperatorInstance) addParamsFuncs(env wazero.HostModuleBuilder) {
exportFunction(env, "getParamValue", i.getParamValue,
[]wapi.ValueType{
wapi.ValueTypeI64, // ParamKey
},
[]wapi.ValueType{wapi.ValueTypeI64}, // Value
)
}

// getParamValue returns the value of a param.
// Params:
// - stack[0] parameter key
// Return value:
// - Uint64 with the param's value, 0 on error
func (i *wasmOperatorInstance) getParamValue(ctx context.Context, m wapi.Module, stack []uint64) {
paramKeyPtr := stack[0]

paramKey, err := stringFromStack(m, paramKeyPtr)
if err != nil {
i.logger.Warnf("getParamValue: reading string from stack: %v", err)
stack[0] = 0
return
}

val, ok := i.paramValues[paramKey]
if !ok {
i.logger.Warnf("getParamValue: param %q not found", paramKey)
stack[0] = 0
return
}

buf := []byte(val)
ret, err := i.writeToGuestMemory(ctx, buf)
if err != nil {
i.logger.Warnf("getParamValue: writing to guest memory: %v", err)
stack[0] = 0
return
}

stack[0] = ret
}
1 change: 1 addition & 0 deletions pkg/operators/wasm/testdata/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ TEST_ARTIFACTS = \
fields \
dataarray \
badguest \
params \
#

all: $(TEST_ARTIFACTS)
Expand Down
Binary file modified pkg/operators/wasm/testdata/badguest.tar
Binary file not shown.
2 changes: 1 addition & 1 deletion pkg/operators/wasm/testdata/badguest/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module main

go 1.22.0
go 1.22.7

require github.com/inspektor-gadget/inspektor-gadget v0.27.0

Expand Down
13 changes: 10 additions & 3 deletions pkg/operators/wasm/testdata/badguest/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ func fieldGet(acc uint32, data uint32, kind uint32) uint64
//go:wasmimport env fieldSet
func fieldSet(acc uint32, data uint32, kind uint32, value uint64) uint32

//go:wasmimport env getParamValue
func getParamValue(key uint64) uint64

func stringToBufPtr(s string) uint64 {
unsafePtr := unsafe.Pointer(unsafe.StringData(s))
return uint64(len(s))<<32 | uint64(uintptr(unsafePtr))
Expand All @@ -99,19 +102,19 @@ func logAndPanic(msg string) {
panic(msg)
}

func assertZero(v uint32, msg string) {
func assertZero[T uint64 | uint32](v T, msg string) {
if v != 0 {
logAndPanic(fmt.Sprintf("%d is not zero: %s", v, msg))
}
}

func assertNonZero(v uint32, msg string) {
func assertNonZero[T uint64 | uint32](v T, msg string) {
if v == 0 {
logAndPanic(fmt.Sprintf("v is zero: %s", msg))
}
}

func assertEqual(v1, v2 uint32, msg string) {
func assertEqual[T uint64 | uint32](v1, v2 T, msg string) {
if v1 != v2 {
logAndPanic(fmt.Sprintf("%d != %d: %s", v1, v2, msg))
}
Expand Down Expand Up @@ -240,6 +243,10 @@ func gadgetInit() int {
fieldGet(fieldHandle, fieldHandle, uint32(api.Kind_Uint32))
fieldGet(dataHandle, dataHandle, uint32(api.Kind_Uint32))

/* Params */
assertZero(getParamValue(stringToBufPtr("non-existing-param")), "getParamValue: not-found")
assertZero(getParamValue(invalidStrPtr), "getParamValue: invalid key ptr")

return 0
}

Expand Down
Binary file added pkg/operators/wasm/testdata/params.tar
Binary file not shown.
1 change: 1 addition & 0 deletions pkg/operators/wasm/testdata/params/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
wasm: program.go
11 changes: 11 additions & 0 deletions pkg/operators/wasm/testdata/params/gadget.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: params_test
params:
wasm:
param-key:
key: param-key
description: param-description
defaultValue: param-default-value
typeHint: param-type-hint
title: param-title
alias: param-alias
isMandatory: true
8 changes: 8 additions & 0 deletions pkg/operators/wasm/testdata/params/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module main

go 1.22.7

require github.com/inspektor-gadget/inspektor-gadget v0.27.0

// use this to be able to compile it locally
replace github.com/inspektor-gadget/inspektor-gadget => ../../../../../
1 change: 1 addition & 0 deletions pkg/operators/wasm/testdata/params/program.bpf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// TODO: a c file is always needed by the gadget to be built
48 changes: 48 additions & 0 deletions pkg/operators/wasm/testdata/params/program.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2024 The Inspektor Gadget authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// This program tries as hard as it can to break the host by calling functions
// with wrong arguments. It uses the low level functions directly as the goal is
// to test the host and not the wrapper API. Tests under dataarray and fields
// test also the higher level API.
package main

import (
api "github.com/inspektor-gadget/inspektor-gadget/wasmapi/go"
)

//export gadgetStart
func gadgetStart() int {
val, err := api.GetParamValue("param-key")
if err != nil {
api.Errorf("failed to get param: %v", err)
return 1
}

const expected = "param-value"
if val != expected {
api.Errorf("param value should be %q, got: %q", expected, val)
return 1
}

_, err = api.GetParamValue("non-existing-param")
if err == nil {
api.Errorf("looking for non-existing-param succeded")
return 1
}

return 0
}

func main() {}
31 changes: 27 additions & 4 deletions pkg/operators/wasm/wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"github.com/hashicorp/go-multierror"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/viper"
"github.com/tetratelabs/wazero"
wapi "github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
Expand Down Expand Up @@ -65,16 +66,34 @@ func (w *wasmOperator) InstantiateImageOperator(
operators.ImageOperatorInstance, error,
) {
instance := &wasmOperatorInstance{
gadgetCtx: gadgetCtx,
handleMap: map[uint32]any{},
logger: gadgetCtx.Logger(),
gadgetCtx: gadgetCtx,
handleMap: map[uint32]any{},
logger: gadgetCtx.Logger(),
paramValues: paramValues,
}

if err := instance.init(gadgetCtx, target, desc); err != nil {
instance.close(gadgetCtx)
return nil, fmt.Errorf("initializing wasm: %w", err)
}

var config *viper.Viper
if configVar, ok := gadgetCtx.GetVar("config"); ok {
config, _ = configVar.(*viper.Viper)
}

if config != nil {
extraParams := map[string]*api.Param{}
err := config.UnmarshalKey("params.wasm", &extraParams)
if err != nil {
return nil, fmt.Errorf("unmarshalling extra params: %w", err)
}

for _, v := range extraParams {
instance.extraParams = append(instance.extraParams, v)
}
}

return instance, nil
}

Expand All @@ -94,6 +113,9 @@ type wasmOperatorInstance struct {
handleMap map[uint32]any
lastHandleIndex uint32
handleLock sync.RWMutex

extraParams api.Params
paramValues map[string]string
}

func (i *wasmOperatorInstance) Name() string {
Expand All @@ -109,7 +131,7 @@ func (i *wasmOperatorInstance) Prepare(gadgetCtx operators.GadgetContext) error
}

func (i *wasmOperatorInstance) ExtraParams(gadgetCtx operators.GadgetContext) api.Params {
return nil
return i.extraParams
}

func (i *wasmOperatorInstance) addHandle(obj any) uint32 {
Expand Down Expand Up @@ -190,6 +212,7 @@ func (i *wasmOperatorInstance) init(
i.addLogFuncs(env)
i.addDataSourceFuncs(env)
i.addFieldFuncs(env)
i.addParamsFuncs(env)

if _, err := env.Instantiate(ctx); err != nil {
return fmt.Errorf("instantiating host module: %w", err)
Expand Down
54 changes: 54 additions & 0 deletions pkg/operators/wasm/wasm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,57 @@ func TestBadGuest(t *testing.T) {
err = runtime.RunGadget(gadgetCtx, nil, params)
require.NoError(t, err, "running gadget")
}

func TestWasmParams(t *testing.T) {
utilstest.RequireRoot(t)

t.Parallel()

myOperator := simple.New("myHandler",
simple.OnStart(func(gadgetCtx operators.GadgetContext) error {
params := gadgetCtx.Params()
found := false
for _, p := range params {
if p.Key == "param-key" {
require.Equal(t, "param-description", p.Description)
require.Equal(t, "param-default-value", p.DefaultValue)
require.Equal(t, "param-type-hint", p.TypeHint)
require.Equal(t, "param-title", p.Title)
require.Equal(t, "param-alias", p.Alias)
require.True(t, p.IsMandatory)

found = true
break
}
}

require.True(t, found, "param not found")
return nil
}),
)

ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
t.Cleanup(cancel)

ociStore, err := orasoci.NewFromTar(ctx, "testdata/params.tar")
require.NoError(t, err, "creating oci store")

gadgetCtx := gadgetcontext.New(
ctx,
"params:latest",
gadgetcontext.WithDataOperators(ocihandler.OciHandler, myOperator),
gadgetcontext.WithOrasReadonlyTarget(ociStore),
)

runtime := local.New()
err = runtime.Init(nil)
require.NoError(t, err, "runtime init")
t.Cleanup(func() { runtime.Close() })

params := map[string]string{
"operator.oci.verify-image": "false",
"operator.oci.wasm.param-key": "param-value",
}
err = runtime.RunGadget(gadgetCtx, nil, params)
require.NoError(t, err, "running gadget")
}
Loading

0 comments on commit 0f85578

Please sign in to comment.