mirror of https://github.com/helm/helm.git
Plugin extism/v1 runtime
Signed-off-by: George Jenkins <gvjenkins@gmail.com>
This commit is contained in:
parent
073c61822d
commit
c8e51b40c2
7
go.mod
7
go.mod
|
@ -14,6 +14,7 @@ require (
|
|||
github.com/cyphar/filepath-securejoin v0.4.1
|
||||
github.com/distribution/distribution/v3 v3.0.0
|
||||
github.com/evanphx/json-patch/v5 v5.9.11
|
||||
github.com/extism/go-sdk v1.7.1
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/fluxcd/cli-utils v0.36.0-flux.14
|
||||
github.com/foxcpp/go-mockdns v1.1.0
|
||||
|
@ -25,13 +26,14 @@ require (
|
|||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/mitchellh/copystructure v1.2.0
|
||||
github.com/moby/term v0.5.2
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/rubenv/sql-migrate v1.8.0
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.7
|
||||
github.com/stretchr/testify v1.11.0
|
||||
github.com/tetratelabs/wazero v1.9.0
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/term v0.34.0
|
||||
|
@ -71,6 +73,7 @@ require (
|
|||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
|
@ -95,6 +98,7 @@ require (
|
|||
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
|
@ -130,6 +134,7 @@ require (
|
|||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
|
|
10
go.sum
10
go.sum
|
@ -77,12 +77,16 @@ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ
|
|||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
|
||||
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
|
||||
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
|
||||
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
|
||||
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
|
||||
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
|
||||
github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
|
@ -164,6 +168,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvH
|
|||
github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
|
@ -311,6 +317,10 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
|
||||
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
|
||||
|
|
|
@ -23,7 +23,6 @@ import (
|
|||
|
||||
// Config interface defines the methods that all plugin type configurations must implement
|
||||
type Config interface {
|
||||
GetType() string
|
||||
Validate() error
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,11 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
extism "github.com/extism/go-sdk"
|
||||
"github.com/tetratelabs/wazero"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"helm.sh/helm/v4/pkg/helmpath"
|
||||
)
|
||||
|
||||
func peekAPIVersion(r io.Reader) (string, error) {
|
||||
|
@ -101,12 +105,22 @@ type prototypePluginManager struct {
|
|||
runtimes map[string]Runtime
|
||||
}
|
||||
|
||||
func newPrototypePluginManager() *prototypePluginManager {
|
||||
func newPrototypePluginManager() (*prototypePluginManager, error) {
|
||||
|
||||
cc, err := wazero.NewCompilationCacheWithDir(helmpath.CachePath("wazero-build"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create wazero compilation cache: %w", err)
|
||||
}
|
||||
|
||||
return &prototypePluginManager{
|
||||
runtimes: map[string]Runtime{
|
||||
"subprocess": &RuntimeSubprocess{},
|
||||
"extism/v1": &RuntimeExtismV1{
|
||||
HostFunctions: map[string]extism.HostFunction{},
|
||||
CompilationCache: cc,
|
||||
},
|
||||
}
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pm *prototypePluginManager) RegisterRuntime(runtimeName string, runtime Runtime) {
|
||||
|
@ -135,7 +149,10 @@ func LoadDir(dirname string) (Plugin, error) {
|
|||
return nil, fmt.Errorf("failed to load plugin %q: %w", dirname, err)
|
||||
}
|
||||
|
||||
pm := newPrototypePluginManager()
|
||||
pm, err := newPrototypePluginManager()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create plugin manager: %w", err)
|
||||
}
|
||||
return pm.CreatePlugin(dirname, m)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ package plugin
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"helm.sh/helm/v4/internal/plugin/schema"
|
||||
)
|
||||
|
||||
// Metadata of a plugin, converted from the "on-disk" legacy or v1 plugin.yaml
|
||||
|
@ -183,6 +185,8 @@ func convertMetadataConfig(pluginType string, configRaw map[string]any) (Config,
|
|||
var config Config
|
||||
|
||||
switch pluginType {
|
||||
case "test/v1":
|
||||
config, err = remarshalConfig[*schema.ConfigTestV1](configRaw)
|
||||
case "cli/v1":
|
||||
config, err = remarshalConfig[*ConfigCLI](configRaw)
|
||||
case "getter/v1":
|
||||
|
@ -205,6 +209,8 @@ func convertMetdataRuntimeConfig(runtimeType string, runtimeConfigRaw map[string
|
|||
switch runtimeType {
|
||||
case "subprocess":
|
||||
runtimeConfig, err = remarshalRuntimeConfig[*RuntimeConfigSubprocess](runtimeConfigRaw)
|
||||
case "extism/v1":
|
||||
runtimeConfig, err = remarshalRuntimeConfig[*RuntimeConfigExtismV1](runtimeConfigRaw)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported plugin runtime type: %q", runtimeType)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/*
|
||||
This file contains a "registry" of supported plugin types.
|
||||
|
||||
It enables "dyanmic" operations on the go type associated with a given plugin type (see: `helm.sh/helm/v4/internal/plugin/schema` package)
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
|
||||
// Create a new instance of the output message type for a given plugin type:
|
||||
|
||||
pluginType := "cli/v1" // for example
|
||||
ptm, ok := pluginTypesIndex[pluginType]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown plugin type %q", pluginType)
|
||||
}
|
||||
|
||||
outputMessageType := reflect.Zero(ptm.outputType).Interface()
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
// Create a new instance of the config type for a given plugin type
|
||||
|
||||
pluginType := "cli/v1" // for example
|
||||
ptm, ok := pluginTypesIndex[pluginType]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
config := reflect.New(ptm.configType).Interface().(Config) // `config` is variable of type `Config`, with
|
||||
|
||||
// validate
|
||||
err := config.Validate()
|
||||
if err != nil { // handle error }
|
||||
|
||||
// assert to concrete type if needed
|
||||
cliConfig := config.(*schema.ConfigCLIV1)
|
||||
|
||||
```
|
||||
*/
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"helm.sh/helm/v4/internal/plugin/schema"
|
||||
)
|
||||
|
||||
type pluginTypeMeta struct {
|
||||
pluginType string
|
||||
inputType reflect.Type
|
||||
outputType reflect.Type
|
||||
configType reflect.Type
|
||||
}
|
||||
|
||||
var pluginTypes = []pluginTypeMeta{
|
||||
{
|
||||
pluginType: "test/v1",
|
||||
inputType: reflect.TypeOf(schema.InputMessageTestV1{}),
|
||||
outputType: reflect.TypeOf(schema.OutputMessageTestV1{}),
|
||||
configType: reflect.TypeOf(schema.ConfigTestV1{}),
|
||||
},
|
||||
{
|
||||
pluginType: "cli/v1",
|
||||
inputType: reflect.TypeOf(schema.InputMessageCLIV1{}),
|
||||
outputType: reflect.TypeOf(schema.OutputMessageCLIV1{}),
|
||||
configType: reflect.TypeOf(ConfigCLI{}),
|
||||
},
|
||||
{
|
||||
pluginType: "getter/v1",
|
||||
inputType: reflect.TypeOf(schema.InputMessageGetterV1{}),
|
||||
outputType: reflect.TypeOf(schema.OutputMessageGetterV1{}),
|
||||
configType: reflect.TypeOf(ConfigGetter{}),
|
||||
},
|
||||
}
|
||||
|
||||
var pluginTypesIndex = func() map[string]*pluginTypeMeta {
|
||||
result := make(map[string]*pluginTypeMeta, len(pluginTypes))
|
||||
for _, m := range pluginTypes {
|
||||
result[m.pluginType] = &m
|
||||
}
|
||||
return result
|
||||
}()
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"helm.sh/helm/v4/internal/plugin/schema"
|
||||
)
|
||||
|
||||
func TestMakeOutputMessage(t *testing.T) {
|
||||
ptm := pluginTypesIndex["getter/v1"]
|
||||
outputType := reflect.Zero(ptm.outputType).Interface()
|
||||
assert.IsType(t, schema.OutputMessageGetterV1{}, outputType)
|
||||
|
||||
}
|
||||
|
||||
func TestMakeConfig(t *testing.T) {
|
||||
ptm := pluginTypesIndex["getter/v1"]
|
||||
config := reflect.New(ptm.configType).Interface().(Config)
|
||||
assert.IsType(t, &ConfigGetter{}, config)
|
||||
}
|
|
@ -15,7 +15,11 @@ limitations under the License.
|
|||
|
||||
package plugin
|
||||
|
||||
import "go.yaml.in/yaml/v3"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
// Runtime represents a plugin runtime (subprocess, extism, etc) ie. how a plugin should be executed
|
||||
// Runtime is responsible for instantiating plugins that implement the runtime
|
||||
|
@ -47,3 +51,25 @@ func remarshalRuntimeConfig[T RuntimeConfig](runtimeData map[string]any) (Runtim
|
|||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// parseEnv takes a list of "KEY=value" environment variable strings
|
||||
// and transforms the result into a map[KEY]=value
|
||||
//
|
||||
// - empty input strings are ignored
|
||||
// - input strings with no value are stored as empty strings
|
||||
// - duplicate keys overwrite earlier values
|
||||
func parseEnv(env []string) map[string]string {
|
||||
result := make(map[string]string, len(env))
|
||||
for _, envVar := range env {
|
||||
parts := strings.SplitN(envVar, "=", 2)
|
||||
if len(parts) > 0 && parts[0] != "" {
|
||||
key := parts[0]
|
||||
var value string
|
||||
if len(parts) > 1 {
|
||||
value = parts[1]
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -0,0 +1,297 @@
|
|||
/*
|
||||
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 ExtistmV1WasmBinaryFilename = "plugin.wasm"
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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"`
|
||||
|
||||
// // An optional set of mappings between the host's filesystem and the paths a plugin can access.
|
||||
// TODO: shuld Helm expose this?
|
||||
//AllowedPaths map[string]string `yaml:"allowedPaths,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)
|
||||
}
|
||||
|
||||
fmt.Printf("Creating extism/v1 plugin %q with config: %+v\n", metadata.Name, rc)
|
||||
|
||||
wasmFile := filepath.Join(pluginDir, ExtistmV1WasmBinaryFilename)
|
||||
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 {
|
||||
tmpDir, err := os.MkdirTemp(os.TempDir(), "helm-plugin-*")
|
||||
slog.Debug("created plugin temp dir", slog.String("dir", tmpDir), 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()))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
manifest, err := buildManifest(p.dir, tmpDir, p.rc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := buildPluginConfig(input, p.r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 marshel 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 marshel 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, ExtistmV1WasmBinaryFilename)
|
||||
|
||||
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, error) {
|
||||
|
||||
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, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
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 (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
extism "github.com/extism/go-sdk"
|
||||
|
||||
"helm.sh/helm/v4/internal/plugin/schema"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type pluginRaw struct {
|
||||
Metadata Metadata
|
||||
Dir string
|
||||
}
|
||||
|
||||
func buildLoadExtismPlugin(t *testing.T, dir string) pluginRaw {
|
||||
t.Helper()
|
||||
|
||||
pluginFile := filepath.Join(dir, PluginFileName)
|
||||
|
||||
metadataData, err := os.ReadFile(pluginFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := loadMetadata(metadataData)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "extism/v1", m.Runtime, "expected plugin runtime to be extism/v1")
|
||||
|
||||
cmd := exec.Command("make", "-C", dir)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
require.NoError(t, cmd.Run(), "failed to build plugin in %q", dir)
|
||||
|
||||
return pluginRaw{
|
||||
Metadata: *m,
|
||||
Dir: dir,
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeConfigExtismV1Validate(t *testing.T) {
|
||||
rc := RuntimeConfigExtismV1{}
|
||||
err := rc.Validate()
|
||||
assert.NoError(t, err, "expected no error for empty RuntimeConfigExtismV1")
|
||||
}
|
||||
|
||||
func TestRuntimeExtismV1InvokePlugin(t *testing.T) {
|
||||
r := RuntimeExtismV1{}
|
||||
|
||||
pr := buildLoadExtismPlugin(t, "testdata/src/extismv1-test")
|
||||
require.Equal(t, "test/v1", pr.Metadata.Type)
|
||||
|
||||
p, err := r.CreatePlugin(pr.Dir, &pr.Metadata)
|
||||
|
||||
assert.NoError(t, err, "expected no error creating plugin")
|
||||
assert.NotNil(t, p, "expected plugin to be created")
|
||||
|
||||
output, err := p.Invoke(t.Context(), &Input{
|
||||
Message: schema.InputMessageTestV1{
|
||||
Name: "Phippy",
|
||||
},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
msg := output.Message.(schema.OutputMessageTestV1)
|
||||
assert.Equal(t, "Hello, Phippy! (6)", msg.Greeting)
|
||||
}
|
||||
|
||||
func TestBuildManifest(t *testing.T) {
|
||||
rc := &RuntimeConfigExtismV1{
|
||||
Memory: RuntimeConfigExtismV1Memory{
|
||||
MaxPages: 8,
|
||||
MaxHTTPResponseBytes: 81920,
|
||||
MaxVarBytes: 8192,
|
||||
},
|
||||
FileSystem: RuntimeConfigExtismV1FileSystem{
|
||||
CreateTempDir: true,
|
||||
},
|
||||
Config: map[string]string{"CONFIG_KEY": "config_value"},
|
||||
AllowedHosts: []string{"example.com", "api.example.com"},
|
||||
Timeout: 5000,
|
||||
}
|
||||
|
||||
expected := extism.Manifest{
|
||||
Wasm: []extism.Wasm{
|
||||
extism.WasmFile{
|
||||
Path: "/path/to/plugin/plugin.wasm",
|
||||
Name: "/path/to/plugin/plugin.wasm",
|
||||
},
|
||||
},
|
||||
Memory: &extism.ManifestMemory{
|
||||
MaxPages: 8,
|
||||
MaxHttpResponseBytes: 81920,
|
||||
MaxVarBytes: 8192,
|
||||
},
|
||||
Config: map[string]string{"CONFIG_KEY": "config_value"},
|
||||
AllowedHosts: []string{"example.com", "api.example.com"},
|
||||
AllowedPaths: map[string]string{"/tmp/foo": "/tmp"},
|
||||
Timeout: 5000,
|
||||
}
|
||||
|
||||
manifest, err := buildManifest("/path/to/plugin", "/tmp/foo", rc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, manifest)
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
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 (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseEnv(t *testing.T) {
|
||||
type testCase struct {
|
||||
env []string
|
||||
expected map[string]string
|
||||
}
|
||||
|
||||
testCases := map[string]testCase{
|
||||
"empty": {
|
||||
env: []string{},
|
||||
expected: map[string]string{},
|
||||
},
|
||||
"single": {
|
||||
env: []string{"KEY=value"},
|
||||
expected: map[string]string{"KEY": "value"},
|
||||
},
|
||||
"multiple": {
|
||||
env: []string{"KEY1=value1", "KEY2=value2"},
|
||||
expected: map[string]string{"KEY1": "value1", "KEY2": "value2"},
|
||||
},
|
||||
"no_value": {
|
||||
env: []string{"KEY1=value1", "KEY2="},
|
||||
expected: map[string]string{"KEY1": "value1", "KEY2": ""},
|
||||
},
|
||||
"duplicate_keys": {
|
||||
env: []string{"KEY=value1", "KEY=value2"},
|
||||
expected: map[string]string{"KEY": "value2"}, // last value should overwrite
|
||||
},
|
||||
"empty_strings": {
|
||||
env: []string{"", "KEY=value", ""},
|
||||
expected: map[string]string{"KEY": "value"},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
result := parseEnv(tc.env)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
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 schema
|
||||
|
||||
type InputMessageTestV1 struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type OutputMessageTestV1 struct {
|
||||
Greeting string
|
||||
}
|
||||
|
||||
type ConfigTestV1 struct{}
|
||||
|
||||
func (c *ConfigTestV1) Validate() error {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
plugin.wasm
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
.DEFAULT: build
|
||||
.PHONY: build test vet
|
||||
|
||||
.PHONY: plugin.wasm
|
||||
plugin.wasm:
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm .
|
||||
|
||||
build: plugin.wasm
|
||||
|
||||
vet:
|
||||
GOOS=wasip1 GOARCH=wasm go vet ./...
|
|
@ -0,0 +1,5 @@
|
|||
module helm.sh/helm/v4/internal/plugin/src/extismv1-test
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require github.com/extism/go-pdk v1.1.3
|
|
@ -0,0 +1,2 @@
|
|||
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
|
||||
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
|
|
@ -0,0 +1,61 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
type InputMessageTestV1 struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type OutputMessageTestV1 struct {
|
||||
Greeting string
|
||||
}
|
||||
|
||||
type ConfigTestV1 struct{}
|
||||
|
||||
func runGetterPluginImpl(input InputMessageTestV1) (*OutputMessageTestV1, error) {
|
||||
name := input.Name
|
||||
return &OutputMessageTestV1{
|
||||
Greeting: fmt.Sprintf("Hello, %s! (%d)", name, len(name)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func RunGetterPlugin() error {
|
||||
var input InputMessageTestV1
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
return fmt.Errorf("failed to parse input json: %w", err)
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("Received input: %+v", input))
|
||||
output, err := runGetterPluginImpl(input)
|
||||
if err != nil {
|
||||
pdk.Log(pdk.LogError, fmt.Sprintf("failed: %s", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("Sending output: %+v", output))
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
return fmt.Errorf("failed to write output json: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:wasmexport helm_plugin_main
|
||||
func HelmPlugin() uint32 {
|
||||
pdk.Log(pdk.LogDebug, "running example-extism-getter plugin")
|
||||
|
||||
if err := RunGetterPlugin(); err != nil {
|
||||
pdk.Log(pdk.LogError, err.Error())
|
||||
pdk.SetError(err)
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func main() {}
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
type: test/v1
|
||||
name: extismv1-test
|
||||
version: 0.1.0
|
||||
runtime: extism/v1
|
Loading…
Reference in New Issue