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 }