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 }