github.com/GoogleContainerTools/kpt@v1.0.0-beta.50.0.20240520170205-c25345ffcbee/internal/fnruntime/wasmtime.go (about)

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