components-contrib/bindings/wasm/output.go

193 lines
5.7 KiB
Go

/*
Copyright 2023 The Dapr 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 implieout.
See the License for the specific language governing permissions and
limitations under the License.
*/
package wasm
import (
"bytes"
"context"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"sync/atomic"
"github.com/stealthrocket/wasi-go/imports/wasi_http"
"github.com/stealthrocket/wasi-go/imports/wasi_http/default_http"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
"github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/internal/wasm"
"github.com/dapr/components-contrib/metadata"
"github.com/dapr/kit/logger"
)
// ExecuteOperation is defined here as it isn't in the bindings package.
const ExecuteOperation bindings.OperationKind = "execute"
type outputBinding struct {
logger logger.Logger
runtimeConfig wazero.RuntimeConfig
meta *wasm.InitMetadata
runtime wazero.Runtime
module wazero.CompiledModule
instanceCounter atomic.Uint64
}
var (
_ bindings.OutputBinding = (*outputBinding)(nil)
_ io.Closer = (*outputBinding)(nil)
)
func NewWasmOutput(logger logger.Logger) bindings.OutputBinding {
return &outputBinding{
logger: logger,
// The below ensures context cancels in-flight wasm functions.
runtimeConfig: wazero.NewRuntimeConfig().
WithCloseOnContextDone(true),
}
}
func (out *outputBinding) Init(ctx context.Context, metadata bindings.Metadata) (err error) {
if out.meta, err = wasm.GetInitMetadata(ctx, metadata.Base); err != nil {
return fmt.Errorf("wasm: failed to parse metadata: %w", err)
}
// Create the runtime, which when closed releases any resources associated with it.
out.runtime = wazero.NewRuntimeWithConfig(ctx, out.runtimeConfig)
// Compile the module, which reduces execution time of Invoke
out.module, err = out.runtime.CompileModule(ctx, out.meta.Guest)
if err != nil {
_ = out.runtime.Close(context.Background())
return fmt.Errorf("wasm: error compiling binary: %w", err)
}
imports := detectImports(out.module.ImportedFunctions())
if _, found := imports[modeWasiP1]; found {
_, err = wasi_snapshot_preview1.Instantiate(ctx, out.runtime)
}
if err != nil {
_ = out.runtime.Close(context.Background())
return fmt.Errorf("wasm: error instantiating host wasi functions: %w", err)
}
if _, found := imports[modeWasiHTTP]; found {
if out.meta.StrictSandbox {
_ = out.runtime.Close(context.Background())
return fmt.Errorf("can not instantiate wasi-http with strict sandbox")
}
err = wasi_http.Instantiate(ctx, out.runtime)
}
if err != nil {
_ = out.runtime.Close(context.Background())
return fmt.Errorf("wasm: error instantiating host wasi-http functions: %w", err)
}
return nil
}
func (out *outputBinding) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
guestName := out.meta.GuestName
if guestName == "" {
guestName = out.module.Name()
}
// Currently, concurrent modules can conflict on name. Make sure we have
// a unique one.
instanceNum := out.instanceCounter.Add(1)
instanceName := guestName + "-" + strconv.FormatUint(instanceNum, 10)
moduleConfig := wasm.NewModuleConfig(out.meta).WithName(instanceName)
// Only assign STDIN if it is present in the request.
if len(req.Data) > 0 {
moduleConfig = moduleConfig.WithStdin(bytes.NewReader(req.Data))
}
// Any STDOUT is returned as a result: capture it into a buffer.
var stdout bytes.Buffer
moduleConfig = moduleConfig.WithStdout(&stdout)
// Set the program name to the binary name
argsSlice := []string{guestName}
// Get any remaining args from configuration
if args := req.Metadata["args"]; args != "" {
argsSlice = append(argsSlice, strings.Split(args, ",")...)
}
moduleConfig = moduleConfig.WithArgs(argsSlice...)
// Instantiating executes the guest's main function (exported as _start).
mod, err := out.runtime.InstantiateModule(ctx, out.module, moduleConfig)
// WASI typically calls proc_exit which exits the module, but just in case
// it doesn't, close the module manually.
_ = mod.Close(ctx)
// Return STDOUT if there was no error.
if err == nil {
return &bindings.InvokeResponse{Data: stdout.Bytes()}, nil
}
return nil, err
}
func (out *outputBinding) Operations() []bindings.OperationKind {
return []bindings.OperationKind{
ExecuteOperation,
}
}
// Close implements io.Closer
func (out *outputBinding) Close() error {
// wazero's runtime closes everything.
if rt := out.runtime; rt != nil {
return rt.Close(context.Background())
}
return nil
}
// In the future
const (
modeDefault importMode = iota
modeWasiP1
modeWasiHTTP
)
type importMode uint
func detectImports(imports []api.FunctionDefinition) map[importMode]bool {
result := make(map[importMode]bool)
for _, f := range imports {
moduleName, _, _ := f.Import()
switch moduleName {
case wasi_snapshot_preview1.ModuleName:
result[modeWasiP1] = true
case default_http.ModuleName:
result[modeWasiHTTP] = true
}
}
return result
}
// GetComponentMetadata returns the metadata of the component.
func (out *outputBinding) GetComponentMetadata() (metadataInfo metadata.MetadataMap) {
metadataStruct := wasm.InitMetadata{}
metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType)
return
}