mirror of https://github.com/helm/helm
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
293 lines
8.5 KiB
293 lines
8.5 KiB
/*
|
|
Copyright The Helm 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 implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
|
|
extism "github.com/extism/go-sdk"
|
|
"github.com/tetratelabs/wazero"
|
|
)
|
|
|
|
const ExtismV1WasmBinaryFilename = "plugin.wasm"
|
|
|
|
// RuntimeConfigExtismV1Memory exposes the Wasm/Extism memory options for the plugin
|
|
type RuntimeConfigExtismV1Memory struct {
|
|
// The max amount of pages the plugin can allocate
|
|
// One page is 64Kib. e.g. 16 pages would require 1MiB.
|
|
// Default is 4 pages (256KiB)
|
|
MaxPages uint32 `yaml:"maxPages,omitempty"`
|
|
|
|
// The max size of an Extism HTTP response in bytes
|
|
// Default is 4096 bytes (4KiB)
|
|
MaxHTTPResponseBytes int64 `yaml:"maxHttpResponseBytes,omitempty"`
|
|
|
|
// The max size of all Extism vars in bytes
|
|
// Default is 4096 bytes (4KiB)
|
|
MaxVarBytes int64 `yaml:"maxVarBytes,omitempty"`
|
|
}
|
|
|
|
// RuntimeConfigExtismV1FileSystem exposes filesystem options for the configuration
|
|
// TODO: should Helm expose AllowedPaths?
|
|
type RuntimeConfigExtismV1FileSystem struct {
|
|
// If specified, a temporary directory will be created and mapped to /tmp in the plugin's filesystem.
|
|
// Data written to the directory will be visible on the host filesystem.
|
|
// The directory will be removed when the plugin invocation completes.
|
|
CreateTempDir bool `yaml:"createTempDir,omitempty"`
|
|
}
|
|
|
|
// RuntimeConfigExtismV1 defines the user-configurable options the plugin's Extism runtime
|
|
// The format loosely follows the Extism Manifest format: https://extism.org/docs/concepts/manifest/
|
|
type RuntimeConfigExtismV1 struct {
|
|
// Describes the limits on the memory the plugin may be allocated.
|
|
Memory RuntimeConfigExtismV1Memory `yaml:"memory"`
|
|
|
|
// The "config" key is a free-form map that can be passed to the plugin.
|
|
// The plugin must interpret arbitrary data this map may contain
|
|
Config map[string]string `yaml:"config,omitempty"`
|
|
|
|
// An optional set of hosts this plugin can communicate with.
|
|
// This only has an effect if the plugin makes HTTP requests.
|
|
// If not specified, then no hosts are allowed.
|
|
AllowedHosts []string `yaml:"allowedHosts,omitempty"`
|
|
|
|
FileSystem RuntimeConfigExtismV1FileSystem `yaml:"fileSystem,omitempty"`
|
|
|
|
// The timeout in milliseconds for the plugin to execute
|
|
Timeout uint64 `yaml:"timeout,omitempty"`
|
|
|
|
// HostFunction names exposed in Helm the plugin may access
|
|
// see: https://extism.org/docs/concepts/host-functions/
|
|
HostFunctions []string `yaml:"hostFunctions,omitempty"`
|
|
|
|
// The name of entry function name to call in the plugin
|
|
// Defaults to "helm_plugin_main".
|
|
EntryFuncName string `yaml:"entryFuncName,omitempty"`
|
|
}
|
|
|
|
var _ RuntimeConfig = (*RuntimeConfigExtismV1)(nil)
|
|
|
|
func (r *RuntimeConfigExtismV1) Validate() error {
|
|
// TODO
|
|
return nil
|
|
}
|
|
|
|
type RuntimeExtismV1 struct {
|
|
HostFunctions map[string]extism.HostFunction
|
|
CompilationCache wazero.CompilationCache
|
|
}
|
|
|
|
var _ Runtime = (*RuntimeExtismV1)(nil)
|
|
|
|
func (r *RuntimeExtismV1) CreatePlugin(pluginDir string, metadata *Metadata) (Plugin, error) {
|
|
|
|
rc, ok := metadata.RuntimeConfig.(*RuntimeConfigExtismV1)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid extism/v1 plugin runtime config type: %T", metadata.RuntimeConfig)
|
|
}
|
|
|
|
wasmFile := filepath.Join(pluginDir, ExtismV1WasmBinaryFilename)
|
|
if _, err := os.Stat(wasmFile); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("wasm binary missing for extism/v1 plugin: %q", wasmFile)
|
|
}
|
|
return nil, fmt.Errorf("failed to stat extism/v1 plugin wasm binary %q: %w", wasmFile, err)
|
|
}
|
|
|
|
return &ExtismV1PluginRuntime{
|
|
metadata: *metadata,
|
|
dir: pluginDir,
|
|
rc: rc,
|
|
r: r,
|
|
}, nil
|
|
}
|
|
|
|
type ExtismV1PluginRuntime struct {
|
|
metadata Metadata
|
|
dir string
|
|
rc *RuntimeConfigExtismV1
|
|
r *RuntimeExtismV1
|
|
}
|
|
|
|
var _ Plugin = (*ExtismV1PluginRuntime)(nil)
|
|
|
|
func (p *ExtismV1PluginRuntime) Metadata() Metadata {
|
|
return p.metadata
|
|
}
|
|
|
|
func (p *ExtismV1PluginRuntime) Dir() string {
|
|
return p.dir
|
|
}
|
|
|
|
func (p *ExtismV1PluginRuntime) Invoke(ctx context.Context, input *Input) (*Output, error) {
|
|
|
|
var tmpDir string
|
|
if p.rc.FileSystem.CreateTempDir {
|
|
tmpDirInner, err := os.MkdirTemp(os.TempDir(), "helm-plugin-*")
|
|
slog.Debug("created plugin temp dir", slog.String("dir", tmpDirInner), slog.String("plugin", p.metadata.Name))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create temp dir for extism compilation cache: %w", err)
|
|
}
|
|
defer func() {
|
|
if err := os.RemoveAll(tmpDir); err != nil {
|
|
slog.Warn("failed to remove plugin temp dir", slog.String("dir", tmpDir), slog.String("plugin", p.metadata.Name), slog.String("error", err.Error()))
|
|
}
|
|
}()
|
|
|
|
tmpDir = tmpDirInner
|
|
}
|
|
|
|
manifest, err := buildManifest(p.dir, tmpDir, p.rc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config := buildPluginConfig(input, p.r)
|
|
|
|
hostFunctions, err := buildHostFunctions(p.r.HostFunctions, p.rc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pe, err := extism.NewPlugin(ctx, manifest, config, hostFunctions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create existing plugin: %w", err)
|
|
}
|
|
|
|
pe.SetLogger(func(logLevel extism.LogLevel, s string) {
|
|
slog.Debug(s, slog.String("level", logLevel.String()), slog.String("plugin", p.metadata.Name))
|
|
})
|
|
|
|
inputData, err := json.Marshal(input.Message)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to json marshal plugin input message: %T: %w", input.Message, err)
|
|
}
|
|
|
|
slog.Debug("plugin input", slog.String("plugin", p.metadata.Name), slog.String("inputData", string(inputData)))
|
|
|
|
entryFuncName := p.rc.EntryFuncName
|
|
if entryFuncName == "" {
|
|
entryFuncName = "helm_plugin_main"
|
|
}
|
|
|
|
exitCode, outputData, err := pe.Call(entryFuncName, inputData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("plugin error: %w", err)
|
|
}
|
|
|
|
if exitCode != 0 {
|
|
return nil, &InvokeExecError{
|
|
Code: int(exitCode),
|
|
}
|
|
}
|
|
|
|
slog.Debug("plugin output", slog.String("plugin", p.metadata.Name), slog.Int("exitCode", int(exitCode)), slog.String("outputData", string(outputData)))
|
|
|
|
outputMessage := reflect.New(pluginTypesIndex[p.metadata.Type].outputType)
|
|
if err := json.Unmarshal(outputData, outputMessage.Interface()); err != nil {
|
|
return nil, fmt.Errorf("failed to json marshal plugin output message: %T: %w", outputMessage, err)
|
|
}
|
|
|
|
output := &Output{
|
|
Message: outputMessage.Elem().Interface(),
|
|
}
|
|
|
|
return output, nil
|
|
}
|
|
|
|
func buildManifest(pluginDir string, tmpDir string, rc *RuntimeConfigExtismV1) (extism.Manifest, error) {
|
|
wasmFile := filepath.Join(pluginDir, ExtismV1WasmBinaryFilename)
|
|
|
|
allowedHosts := rc.AllowedHosts
|
|
if allowedHosts == nil {
|
|
allowedHosts = []string{}
|
|
}
|
|
|
|
allowedPaths := map[string]string{}
|
|
if tmpDir != "" {
|
|
allowedPaths[tmpDir] = "/tmp"
|
|
}
|
|
|
|
return extism.Manifest{
|
|
Wasm: []extism.Wasm{
|
|
extism.WasmFile{
|
|
Path: wasmFile,
|
|
Name: wasmFile,
|
|
},
|
|
},
|
|
Memory: &extism.ManifestMemory{
|
|
MaxPages: rc.Memory.MaxPages,
|
|
MaxHttpResponseBytes: rc.Memory.MaxHTTPResponseBytes,
|
|
MaxVarBytes: rc.Memory.MaxVarBytes,
|
|
},
|
|
Config: rc.Config,
|
|
AllowedHosts: allowedHosts,
|
|
AllowedPaths: allowedPaths,
|
|
Timeout: rc.Timeout,
|
|
}, nil
|
|
}
|
|
|
|
func buildPluginConfig(input *Input, r *RuntimeExtismV1) extism.PluginConfig {
|
|
mc := wazero.NewModuleConfig().
|
|
WithSysWalltime()
|
|
if input.Stdin != nil {
|
|
mc = mc.WithStdin(input.Stdin)
|
|
}
|
|
if input.Stdout != nil {
|
|
mc = mc.WithStdout(input.Stdout)
|
|
}
|
|
if input.Stderr != nil {
|
|
mc = mc.WithStderr(input.Stderr)
|
|
}
|
|
if len(input.Env) > 0 {
|
|
env := parseEnv(input.Env)
|
|
for k, v := range env {
|
|
mc = mc.WithEnv(k, v)
|
|
}
|
|
}
|
|
|
|
config := extism.PluginConfig{
|
|
ModuleConfig: mc,
|
|
RuntimeConfig: wazero.NewRuntimeConfigCompiler().
|
|
WithCloseOnContextDone(true).
|
|
WithCompilationCache(r.CompilationCache),
|
|
EnableWasi: true,
|
|
EnableHttpResponseHeaders: true,
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
func buildHostFunctions(hostFunctions map[string]extism.HostFunction, rc *RuntimeConfigExtismV1) ([]extism.HostFunction, error) {
|
|
result := make([]extism.HostFunction, len(rc.HostFunctions))
|
|
for _, fnName := range rc.HostFunctions {
|
|
fn, ok := hostFunctions[fnName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("plugin requested host function %q not found", fnName)
|
|
}
|
|
|
|
result = append(result, fn)
|
|
}
|
|
|
|
return result, nil
|
|
}
|