github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/fnruntime/wasmtime.go (about)

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package fnruntime
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"io"
    21  	"log"
    22  	"os"
    23  
    24  	wasmtime "github.com/bytecodealliance/wasmtime-go"
    25  	"github.com/prep/wasmexec"
    26  	"github.com/prep/wasmexec/wasmtimexec"
    27  	"sigs.k8s.io/kustomize/kyaml/yaml"
    28  )
    29  
    30  type WasmtimeFn struct {
    31  	wasmexec.Memory
    32  	*wasmtime.Instance
    33  	store *wasmtime.Store
    34  
    35  	gomod *wasmexec.Module
    36  
    37  	spFn     *wasmtime.Func
    38  	resumeFn *wasmtime.Func
    39  
    40  	loader WasmLoader
    41  }
    42  
    43  func NewWasmtimeFn(loader WasmLoader) (*WasmtimeFn, error) {
    44  	f := &WasmtimeFn{
    45  		loader: loader,
    46  	}
    47  	wasmFileReadCloser, err := loader.getReadCloser()
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  	defer wasmFileReadCloser.Close()
    52  	// Create a module out of the Wasm file.
    53  	data, err := io.ReadAll(wasmFileReadCloser)
    54  	if err != nil {
    55  		return nil, fmt.Errorf("unable to read wasm content from reader: %w", err)
    56  	}
    57  
    58  	// Create the engine and store.
    59  	config := wasmtime.NewConfig()
    60  	err = config.CacheConfigLoadDefault()
    61  	if err != nil {
    62  		return nil, fmt.Errorf("failed to config cache in wasmtime")
    63  	}
    64  	engine := wasmtime.NewEngineWithConfig(config)
    65  
    66  	module, err := wasmtime.NewModule(engine, data)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  	f.store = wasmtime.NewStore(engine)
    71  
    72  	linker := wasmtime.NewLinker(engine)
    73  	f.gomod, err = wasmtimexec.Import(f.store, linker, f)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	// Create an instance of the module.
    79  	if f.Instance, err = linker.Instantiate(f.store, module); err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	return f, nil
    84  }
    85  
    86  // Run runs the executable file which reads the input from r and
    87  // writes the output to w.
    88  func (f *WasmtimeFn) Run(r io.Reader, w io.Writer) error {
    89  	// Fetch the memory export and set it on the instance, making the memory
    90  	// accessible by the imports.
    91  	ext := f.GetExport(f.store, "mem")
    92  	if ext == nil {
    93  		return errors.New("unable to find memory export")
    94  	}
    95  
    96  	mem := ext.Memory()
    97  	if mem == nil {
    98  		return errors.New("mem: export is not memory")
    99  	}
   100  
   101  	f.Memory = wasmexec.NewMemory(mem.UnsafeData(f.store))
   102  
   103  	// Fetch the getsp function and reference it on the instance.
   104  	spFn := f.GetExport(f.store, "getsp")
   105  	if spFn == nil {
   106  		return errors.New("getsp: missing export")
   107  	}
   108  
   109  	if f.spFn = spFn.Func(); f.spFn == nil {
   110  		return errors.New("getsp: export is not a function")
   111  	}
   112  
   113  	// Fetch the resume function and reference it on the instance.
   114  	resumeFn := f.GetExport(f.store, "resume")
   115  	if resumeFn == nil {
   116  		return errors.New("resume: missing export")
   117  	}
   118  
   119  	if f.resumeFn = resumeFn.Func(); f.resumeFn == nil {
   120  		return errors.New("resume: export is not a function")
   121  	}
   122  
   123  	// Set the args and the environment variables.
   124  	argc, argv, err := wasmexec.SetArgs(f.Memory, []string{"kpt-fn-wasm-wasmtime"}, []string{})
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	// Fetch the "run" function and call it. This starts the program.
   130  	runFn := f.GetFunc(f.store, "run")
   131  	if runFn == nil {
   132  		return errors.New("run: missing export")
   133  	}
   134  
   135  	if _, err = runFn.Call(f.store, argc, argv); err != nil {
   136  		return err
   137  	}
   138  	resourceList, err := io.ReadAll(r)
   139  	if err != nil {
   140  		return err
   141  	}
   142  	result, err := f.gomod.Call(jsEntrypointFunction, string(resourceList))
   143  	if err != nil {
   144  		return fmt.Errorf("unable to invoke %v: %v", jsEntrypointFunction, err)
   145  	}
   146  	// We expect `result` to be a *wasmexec.jsString (which is not exportable) with
   147  	// the following definition: type jsString struct { data string }. It will look
   148  	// like `&{realPayload}`
   149  	resultStr := fmt.Sprintf("%s", result)
   150  	resultStr = resultStr[2 : len(resultStr)-1]
   151  	// Try to parse the output as yaml.
   152  	if _, err = yaml.Parse(resultStr); err != nil {
   153  		return errors.New(resultStr)
   154  	}
   155  	if _, err = w.Write([]byte(resultStr)); err != nil {
   156  		return fmt.Errorf("unable to write the output resource list: %w", err)
   157  	}
   158  	return f.loader.cleanup()
   159  }
   160  
   161  var _ wasmexec.Instance = &WasmtimeFn{}
   162  
   163  func (f *WasmtimeFn) GetSP() (uint32, error) {
   164  	val, err := f.spFn.Call(f.store)
   165  	if err != nil {
   166  		return 0, err
   167  	}
   168  
   169  	sp, ok := val.(int32)
   170  	if !ok {
   171  		return 0, fmt.Errorf("getsp: %T: expected an int32 return value", sp)
   172  	}
   173  
   174  	return uint32(sp), nil
   175  }
   176  
   177  func (f *WasmtimeFn) Resume() error {
   178  	_, err := f.resumeFn.Call(f.store)
   179  	return err
   180  }
   181  
   182  // Write implements the wasmexec.fdWriter interface.
   183  func (f *WasmtimeFn) Write(fd int, b []byte) (n int, err error) {
   184  	switch fd {
   185  	case 1, 2:
   186  		n, err = os.Stdout.Write(b)
   187  	default:
   188  		err = fmt.Errorf("%d: invalid file descriptor", fd)
   189  	}
   190  
   191  	return n, err
   192  }
   193  
   194  // Error implements the wasmexec.errorLogger interface
   195  func (f *WasmtimeFn) Error(format string, params ...interface{}) {
   196  	log.Printf("ERROR: "+format+"\n", params...)
   197  }