Create plugin manager

Signed-off-by: George Jenkins <gvjenkins@gmail.com>
pull/31226/head
George Jenkins 1 month ago
parent 618b14a772
commit abd4a326f0

@ -1,24 +0,0 @@
/*
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
// Descriptor describes a plugin to find
type Descriptor struct {
// Name is the name of the plugin
Name string
// Type is the type of the plugin (cli, getter, postrenderer)
Type string
}

@ -28,7 +28,7 @@ An example of a plugin invocation:
d := plugin.Descriptor{
Type: "example/v1", //
}
plgs, err := plugin.FindPlugins([]string{settings.PluginsDirectory}, d)
plgs, err := settings.PluginCatalog.FindPlugins(d)
for _, plg := range plgs {
input := &plugin.Input{

@ -44,7 +44,7 @@ type HTTPInstaller struct {
}
// NewHTTPInstaller creates a new HttpInstaller.
func NewHTTPInstaller(source string) (*HTTPInstaller, error) {
func NewHTTPInstaller(settings *cli.EnvSettings, source string) (*HTTPInstaller, error) {
key, err := cache.Key(source)
if err != nil {
return nil, err
@ -55,7 +55,7 @@ func NewHTTPInstaller(source string) (*HTTPInstaller, error) {
return nil, err
}
get, err := getter.All(new(cli.EnvSettings)).ByScheme("http")
get, err := getter.All(settings).ByScheme("http")
if err != nil {
return nil, err
}

@ -32,6 +32,7 @@ import (
"testing"
"helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v4/pkg/helmpath"
)
@ -81,6 +82,8 @@ func mockArchiveServer() *httptest.Server {
func TestHTTPInstaller(t *testing.T) {
ensure.HelmHome(t)
settings := cli.New()
srv := mockArchiveServer()
defer srv.Close()
source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
@ -89,7 +92,7 @@ func TestHTTPInstaller(t *testing.T) {
t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
}
i, err := NewForSource(source, "0.0.1")
i, err := NewForSource(settings, source, "0.0.1")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
@ -129,6 +132,9 @@ func TestHTTPInstaller(t *testing.T) {
func TestHTTPInstallerNonExistentVersion(t *testing.T) {
ensure.HelmHome(t)
settings := cli.New()
srv := mockArchiveServer()
defer srv.Close()
source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
@ -137,7 +143,7 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) {
t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
}
i, err := NewForSource(source, "0.0.2")
i, err := NewForSource(settings, source, "0.0.2")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
@ -161,16 +167,20 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) {
}
func TestHTTPInstallerUpdate(t *testing.T) {
ensure.HelmHome(t)
settings := cli.New()
srv := mockArchiveServer()
defer srv.Close()
source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
ensure.HelmHome(t)
if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
}
i, err := NewForSource(source, "0.0.1")
i, err := NewForSource(settings, source, "0.0.1")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}

@ -24,6 +24,7 @@ import (
"strings"
"helm.sh/helm/v4/internal/plugin"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/registry"
)
@ -136,7 +137,7 @@ func Update(i Installer) error {
}
// NewForSource determines the correct Installer for the given source.
func NewForSource(source, version string) (Installer, error) {
func NewForSource(settings *cli.EnvSettings, source, version string) (Installer, error) {
// Check if source is an OCI registry reference
if strings.HasPrefix(source, fmt.Sprintf("%s://", registry.OCIScheme)) {
return NewOCIInstaller(source)
@ -145,7 +146,7 @@ func NewForSource(source, version string) (Installer, error) {
if isLocalReference(source) {
return NewLocalInstaller(source)
} else if isRemoteHTTPArchive(source) {
return NewHTTPInstaller(source)
return NewHTTPInstaller(settings, source)
}
return NewVCSInstaller(source, version)
}

@ -24,6 +24,7 @@ import (
"testing"
"helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/helmpath"
)
@ -31,14 +32,16 @@ var _ Installer = new(LocalInstaller)
func TestLocalInstaller(t *testing.T) {
ensure.HelmHome(t)
// Make a temp dir
settings := cli.New()
tdir := t.TempDir()
if err := os.WriteFile(filepath.Join(tdir, "plugin.yaml"), []byte{}, 0644); err != nil {
t.Fatal(err)
}
source := "../testdata/plugdir/good/echo-v1"
i, err := NewForSource(source, "")
i, err := NewForSource(settings, source, "")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
@ -54,8 +57,12 @@ func TestLocalInstaller(t *testing.T) {
}
func TestLocalInstallerNotAFolder(t *testing.T) {
ensure.HelmHome(t)
settings := cli.New()
source := "../testdata/plugdir/good/echo-v1/plugin.yaml"
i, err := NewForSource(source, "")
i, err := NewForSource(settings, source, "")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
@ -116,8 +123,10 @@ func TestLocalInstallerTarball(t *testing.T) {
t.Fatal(err)
}
settings := cli.New()
// Test installation
i, err := NewForSource(tarballPath, "")
i, err := NewForSource(settings, tarballPath, "")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}

@ -61,12 +61,12 @@ func validatePluginName(pluginRoot string, expectedName string) error {
}
// Load plugin.yaml to get the actual name
p, err := plugin.LoadDir(pluginRoot)
pr, err := plugin.LoadDirRaw(pluginRoot)
if err != nil {
return fmt.Errorf("failed to load plugin from %s: %w", pluginRoot, err)
}
m := p.Metadata()
m := pr.Metadata
actualName := m.Name
// For now, just log a warning if names don't match

@ -25,6 +25,7 @@ import (
"github.com/Masterminds/vcs"
"helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/helmpath"
)
@ -52,6 +53,8 @@ func (r *testRepo) UpdateVersion(version string) error {
func TestVCSInstaller(t *testing.T) {
ensure.HelmHome(t)
settings := cli.New()
if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
}
@ -63,7 +66,7 @@ func TestVCSInstaller(t *testing.T) {
tags: []string{"0.1.0", "0.1.1"},
}
i, err := NewForSource(source, "~0.1.0")
i, err := NewForSource(settings, source, "~0.1.0")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
@ -106,10 +109,12 @@ func TestVCSInstaller(t *testing.T) {
func TestVCSInstallerNonExistentVersion(t *testing.T) {
ensure.HelmHome(t)
settings := cli.New()
source := "https://github.com/adamreese/helm-env"
version := "0.2.0"
i, err := NewForSource(source, version)
i, err := NewForSource(settings, source, version)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
@ -130,9 +135,11 @@ func TestVCSInstallerNonExistentVersion(t *testing.T) {
func TestVCSInstallerUpdate(t *testing.T) {
ensure.HelmHome(t)
settings := cli.New()
source := "https://github.com/adamreese/helm-env"
i, err := NewForSource(source, "")
i, err := NewForSource(settings, source, "")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}

@ -17,18 +17,22 @@ package plugin
import (
"bytes"
"errors"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
extism "github.com/extism/go-sdk"
"github.com/tetratelabs/wazero"
"go.yaml.in/yaml/v3"
"helm.sh/helm/v4/pkg/helmpath"
)
// PluginRaw is an "uninitialized" plugin that has not been bound to a runtime
type PluginRaw struct { //nolint:revive
Metadata Metadata
Dir string
}
func peekAPIVersion(r io.Reader) (string, error) {
type apiVersion struct {
APIVersion string `yaml:"apiVersion"`
@ -101,166 +105,93 @@ func loadMetadata(metadataData []byte) (*Metadata, error) {
return nil, fmt.Errorf("invalid plugin apiVersion: %q", apiVersion)
}
type prototypePluginManager struct {
runtimes map[string]Runtime
}
func newPrototypePluginManager() (*prototypePluginManager, error) {
cc, err := wazero.NewCompilationCacheWithDir(helmpath.CachePath("wazero-build"))
func GlobPluginDirs(baseDir string) ([]string, error) {
// We want baseDir/*/plugin.yaml
scanpath := filepath.Join(baseDir, "*", PluginFileName)
matches, err := filepath.Glob(scanpath)
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) {
pm.runtimes[runtimeName] = runtime
}
func (pm *prototypePluginManager) CreatePlugin(pluginPath string, metadata *Metadata) (Plugin, error) {
rt, ok := pm.runtimes[metadata.Runtime]
if !ok {
return nil, fmt.Errorf("unsupported plugin runtime type: %q", metadata.Runtime)
return nil, fmt.Errorf("failed to search for plugins in %q: %w", scanpath, err)
}
return rt.CreatePlugin(pluginPath, metadata)
return matches, nil
}
// LoadDir loads a plugin from the given directory.
func LoadDir(dirname string) (Plugin, error) {
pluginfile := filepath.Join(dirname, PluginFileName)
// LoadDir loads a plugin source from the given directory
func LoadDirRaw(pluginDir string) (*PluginRaw, error) {
pluginfile := filepath.Join(pluginDir, PluginFileName)
metadataData, err := os.ReadFile(pluginfile)
if err != nil {
return nil, fmt.Errorf("failed to read plugin at %q: %w", pluginfile, err)
}
m, err := loadMetadata(metadataData)
metadata, err := loadMetadata(metadataData)
if err != nil {
return nil, fmt.Errorf("failed to load plugin %q: %w", dirname, err)
return nil, err
}
pm, err := newPrototypePluginManager()
if err != nil {
return nil, fmt.Errorf("failed to create plugin manager: %w", err)
pluginRaw := PluginRaw{
Metadata: *metadata,
Dir: pluginDir,
}
return pm.CreatePlugin(dirname, m)
return &pluginRaw, nil
}
// LoadAll loads all plugins found beneath the base directory.
//
// This scans only one directory level.
func LoadAll(basedir string) ([]Plugin, error) {
var plugins []Plugin
// We want basedir/*/plugin.yaml
scanpath := filepath.Join(basedir, "*", PluginFileName)
matches, err := filepath.Glob(scanpath)
if err != nil {
return nil, fmt.Errorf("failed to search for plugins in %q: %w", scanpath, err)
}
// findFunc is a function that finds plugin directories
type findFunc func(pluginsDir string) ([]string, error)
// empty dir should load
if len(matches) == 0 {
return plugins, nil
func NewDirLoader(store *Store, findFn findFunc) *DirLoader {
return &DirLoader{
Store: store,
FindFunc: findFn,
}
for _, yamlFile := range matches {
dir := filepath.Dir(yamlFile)
p, err := LoadDir(dir)
if err != nil {
return plugins, err
}
plugins = append(plugins, p)
}
return plugins, detectDuplicates(plugins)
}
// findFunc is a function that finds plugins in a directory
type findFunc func(pluginsDir string) ([]Plugin, error)
// filterFunc is a function that filters plugins
type filterFunc func(Plugin) bool
// FindPlugins returns a list of plugins that match the descriptor
func FindPlugins(pluginsDirs []string, descriptor Descriptor) ([]Plugin, error) {
return findPlugins(pluginsDirs, LoadAll, makeDescriptorFilter(descriptor))
type DirLoader struct {
Store *Store
FindFunc func(string) ([]string, error)
}
// findPlugins is the internal implementation that uses the find and filter functions
func findPlugins(pluginsDirs []string, findFn findFunc, filterFn filterFunc) ([]Plugin, error) {
var found []Plugin
for _, pluginsDir := range pluginsDirs {
ps, err := findFn(pluginsDir)
func (l *DirLoader) Load(baseDirs []string) error {
store := NewStore()
errs := []error{}
for _, baseDir := range baseDirs {
slog.Debug("Loading plugins", "directory", baseDir)
matches, err := l.FindFunc(baseDir)
if err != nil {
return nil, err
errs = append(errs, fmt.Errorf("failed to search for plugins in %q: %w", baseDir, err))
continue
}
for _, p := range ps {
if filterFn(p) {
found = append(found, p)
for _, yamlFile := range matches {
dir := filepath.Dir(yamlFile)
plugRaw, err := LoadDirRaw(dir)
if err != nil {
errs = append(errs, fmt.Errorf("failed to load plugin %q: %w", dir, err))
continue
}
}
}
return found, nil
}
// makeDescriptorFilter creates a filter function from a descriptor
// Additional plugin filter criteria we wish to support can be added here
func makeDescriptorFilter(descriptor Descriptor) filterFunc {
return func(p Plugin) bool {
// If name is specified, it must match
if descriptor.Name != "" && p.Metadata().Name != descriptor.Name {
return false
actualPlugRaw, loaded := store.LoadOrStore(plugRaw)
if loaded {
errs = append(errs, fmt.Errorf(
"two plugins claim the name %q at %q and %q",
plugRaw.Metadata.Name,
actualPlugRaw.Dir,
plugRaw.Dir,
))
continue
}
}
// If type is specified, it must match
if descriptor.Type != "" && p.Metadata().Type != descriptor.Type {
return false
}
return true
}
}
// FindPlugin returns a single plugin that matches the descriptor
func FindPlugin(dirs []string, descriptor Descriptor) (Plugin, error) {
plugins, err := FindPlugins(dirs, descriptor)
if err != nil {
return nil, err
if err := errors.Join(errs...); err != nil {
return err
}
if len(plugins) > 0 {
return plugins[0], nil
}
return nil, fmt.Errorf("plugin: %+v not found", descriptor)
}
func detectDuplicates(plugs []Plugin) error {
names := map[string]string{}
for _, plug := range plugs {
if oldpath, ok := names[plug.Metadata().Name]; ok {
return fmt.Errorf(
"two plugins claim the name %q at %q and %q",
plug.Metadata().Name,
oldpath,
plug.Dir(),
)
}
names[plug.Metadata().Name] = plug.Dir()
}
// Atomicly replace the store's plugins with the newly loaded plugins
l.Store.plugins = store.plugins
return nil
}

@ -17,13 +17,10 @@ package plugin
import (
"bytes"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/plugin/schema"
)
func TestPeekAPIVersion(t *testing.T) {
@ -61,210 +58,3 @@ name: "test-plugin"
assert.Error(t, err)
}
}
func TestLoadDir(t *testing.T) {
makeMetadata := func(apiVersion string) Metadata {
usage := "hello [params]..."
if apiVersion == "legacy" {
usage = "" // Legacy plugins don't have Usage field for command syntax
}
return Metadata{
APIVersion: apiVersion,
Name: fmt.Sprintf("hello-%s", apiVersion),
Version: "0.1.0",
Type: "cli/v1",
Runtime: "subprocess",
Config: &schema.ConfigCLIV1{
Usage: usage,
ShortHelp: "echo hello message",
LongHelp: "description",
IgnoreFlags: true,
},
RuntimeConfig: &RuntimeConfigSubprocess{
PlatformCommand: []PlatformCommand{
{OperatingSystem: "linux", Architecture: "", Command: "sh", Args: []string{"-c", "${HELM_PLUGIN_DIR}/hello.sh"}},
{OperatingSystem: "windows", Architecture: "", Command: "pwsh", Args: []string{"-c", "${HELM_PLUGIN_DIR}/hello.ps1"}},
},
PlatformHooks: map[string][]PlatformCommand{
Install: {
{OperatingSystem: "linux", Architecture: "", Command: "sh", Args: []string{"-c", "echo \"installing...\""}},
{OperatingSystem: "windows", Architecture: "", Command: "pwsh", Args: []string{"-c", "echo \"installing...\""}},
},
},
expandHookArgs: apiVersion == "legacy",
},
}
}
testCases := map[string]struct {
dirname string
apiVersion string
expect Metadata
}{
"legacy": {
dirname: "testdata/plugdir/good/hello-legacy",
apiVersion: "legacy",
expect: makeMetadata("legacy"),
},
"v1": {
dirname: "testdata/plugdir/good/hello-v1",
apiVersion: "v1",
expect: makeMetadata("v1"),
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
plug, err := LoadDir(tc.dirname)
require.NoError(t, err, "error loading plugin from %s", tc.dirname)
assert.Equal(t, tc.dirname, plug.Dir())
assert.EqualValues(t, tc.expect, plug.Metadata())
})
}
}
func TestLoadDirDuplicateEntries(t *testing.T) {
testCases := map[string]string{
"legacy": "testdata/plugdir/bad/duplicate-entries-legacy",
"v1": "testdata/plugdir/bad/duplicate-entries-v1",
}
for name, dirname := range testCases {
t.Run(name, func(t *testing.T) {
_, err := LoadDir(dirname)
assert.Error(t, err)
})
}
}
func TestLoadDirGetter(t *testing.T) {
dirname := "testdata/plugdir/good/getter"
expect := Metadata{
Name: "getter",
Version: "1.2.3",
Type: "getter/v1",
APIVersion: "v1",
Runtime: "subprocess",
Config: &schema.ConfigGetterV1{
Protocols: []string{"myprotocol", "myprotocols"},
},
RuntimeConfig: &RuntimeConfigSubprocess{
ProtocolCommands: []SubprocessProtocolCommand{
{
Protocols: []string{"myprotocol", "myprotocols"},
PlatformCommand: []PlatformCommand{{Command: "echo getter"}},
},
},
},
}
plug, err := LoadDir(dirname)
require.NoError(t, err)
assert.Equal(t, dirname, plug.Dir())
assert.Equal(t, expect, plug.Metadata())
}
func TestPostRenderer(t *testing.T) {
dirname := "testdata/plugdir/good/postrenderer-v1"
expect := Metadata{
Name: "postrenderer-v1",
Version: "1.2.3",
Type: "postrenderer/v1",
APIVersion: "v1",
Runtime: "subprocess",
Config: &schema.ConfigPostRendererV1{},
RuntimeConfig: &RuntimeConfigSubprocess{
PlatformCommand: []PlatformCommand{
{
Command: "${HELM_PLUGIN_DIR}/sed-test.sh",
},
},
},
}
plug, err := LoadDir(dirname)
require.NoError(t, err)
assert.Equal(t, dirname, plug.Dir())
assert.Equal(t, expect, plug.Metadata())
}
func TestDetectDuplicates(t *testing.T) {
plugs := []Plugin{
mockSubprocessCLIPlugin(t, "foo"),
mockSubprocessCLIPlugin(t, "bar"),
}
if err := detectDuplicates(plugs); err != nil {
t.Error("no duplicates in the first set")
}
plugs = append(plugs, mockSubprocessCLIPlugin(t, "foo"))
if err := detectDuplicates(plugs); err == nil {
t.Error("duplicates in the second set")
}
}
func TestLoadAll(t *testing.T) {
// Verify that empty dir loads:
{
plugs, err := LoadAll("testdata")
require.NoError(t, err)
assert.Len(t, plugs, 0)
}
basedir := "testdata/plugdir/good"
plugs, err := LoadAll(basedir)
require.NoError(t, err)
require.NotEmpty(t, plugs, "expected plugins to be loaded from %s", basedir)
plugsMap := map[string]Plugin{}
for _, p := range plugs {
plugsMap[p.Metadata().Name] = p
}
assert.Len(t, plugsMap, 7)
assert.Contains(t, plugsMap, "downloader")
assert.Contains(t, plugsMap, "echo-legacy")
assert.Contains(t, plugsMap, "echo-v1")
assert.Contains(t, plugsMap, "getter")
assert.Contains(t, plugsMap, "hello-legacy")
assert.Contains(t, plugsMap, "hello-v1")
assert.Contains(t, plugsMap, "postrenderer-v1")
}
func TestFindPlugins(t *testing.T) {
cases := []struct {
name string
plugdirs string
expected int
}{
{
name: "plugdirs is empty",
plugdirs: "",
expected: 0,
},
{
name: "plugdirs isn't dir",
plugdirs: "./plugin_test.go",
expected: 0,
},
{
name: "plugdirs doesn't have plugin",
plugdirs: ".",
expected: 0,
},
{
name: "normal",
plugdirs: "./testdata/plugdir/good",
expected: 7,
},
}
for _, c := range cases {
t.Run(t.Name(), func(t *testing.T) {
plugin, err := LoadAll(c.plugdirs)
require.NoError(t, err)
assert.Len(t, plugin, c.expected, "expected %d plugins, got %d", c.expected, len(plugin))
})
}
}

@ -0,0 +1,216 @@
/*
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 (
"errors"
"fmt"
"sync"
"sync/atomic"
)
// Descriptor describes a plugin to find
type Descriptor struct {
// Name is the name of the plugin
Name string
// Type is the type of the plugin (cli/v1, getter/v1, postrenderer/v1, etc)
Type string
}
// Catalog is an interface for finding plugins
type Catalog interface {
FindPlugin(Descriptor) (Plugin, error)
FindPlugins(Descriptor) ([]Plugin, error)
}
// NewStore creates a new, empty plugin store
func NewStore() Store {
s := Store{}
s.plugins.Store(&sync.Map{})
return s
}
// Store is a concurrent access safe store for plugins
// Specifically, it is a wrapper around sync.Map for *PluginRaw
// It uses atomic.Value to allow for safe replacement of the underlying sync.Map
// Providing concurrency safe iteration over all plugins (for filtering), and name-based lookup
type Store struct {
plugins atomic.Value
}
func (s *Store) Store(pr *PluginRaw) {
plugins := s.plugins.Load().(*sync.Map)
plugins.Store(pr.Metadata.Name, pr)
}
func (s *Store) LoadOrStore(pr *PluginRaw) (*PluginRaw, bool) {
plugins := s.plugins.Load().(*sync.Map)
actual, loaded := plugins.LoadOrStore(pr.Metadata.Name, pr)
return actual.(*PluginRaw), loaded
}
func (s *Store) Load(name string) *PluginRaw {
plugins := s.plugins.Load().(*sync.Map)
v, ok := plugins.Load(name)
if !ok {
return nil
}
return v.(*PluginRaw)
}
func (s *Store) Range(cb func(*PluginRaw)) {
plugins := s.plugins.Load().(*sync.Map)
plugins.Range(func(_ any, value any) bool {
cb(value.(*PluginRaw))
return true
})
}
func (s *Store) Delete(pluginName string) {
plugins := s.plugins.Load().(*sync.Map)
plugins.Delete(pluginName)
}
type Manager struct {
runtimes map[string]Runtime
Store Store
}
// func NewManager(baseDirs []string) *Manager {
func NewManager() *Manager {
pm := Manager{
//baseDirs: baseDirs,
runtimes: map[string]Runtime{},
}
return &pm
}
func (m *Manager) RegisterRuntime(runtimeName string, runtime Runtime) {
m.runtimes[runtimeName] = runtime
}
func (m *Manager) RetriveRuntime(runtimeName string) Runtime {
return m.runtimes[runtimeName]
}
func (m *Manager) Catalog() Catalog {
return &PluginManagerCatalog{Manager: m}
}
func (m *Manager) FindPluginsRaw(filterFn filterFunc) []*PluginRaw {
results := make([]*PluginRaw, 0, 10)
m.Store.Range(func(pluginRaw *PluginRaw) {
if filterFn(&pluginRaw.Metadata) {
results = append(results, pluginRaw)
}
})
return results
}
func (m *Manager) CreatePlugin(pluginRaw *PluginRaw) (Plugin, error) {
rt, ok := m.runtimes[pluginRaw.Metadata.Runtime]
if !ok {
return nil, fmt.Errorf("unsupported plugin runtime type: %q", pluginRaw.Metadata.Runtime)
}
return rt.CreatePlugin(pluginRaw.Dir, &pluginRaw.Metadata)
}
// filterFunc is a function that filters plugins
type filterFunc func(m *Metadata) bool
// makeDescriptorFilter creates a filter function from a descriptor
// Additional plugin filter criteria we wish to support can be added here
func makeDescriptorFilter(descriptor Descriptor) filterFunc {
return func(m *Metadata) bool {
// If name is specified, it must match
if descriptor.Name != "" && m.Name != descriptor.Name {
return false
}
// If type is specified, it must match
if descriptor.Type != "" && m.Type != descriptor.Type {
return false
}
return true
}
}
type PluginManagerCatalog struct {
Manager *Manager
}
func (c *PluginManagerCatalog) FindPlugin(d Descriptor) (Plugin, error) {
filterFn := makeDescriptorFilter(d)
pluginsRaw := c.Manager.FindPluginsRaw(filterFn)
if len(pluginsRaw) == 0 {
return nil, nil
}
if len(pluginsRaw) > 1 {
return nil, fmt.Errorf("multiple matching plugins found")
}
return c.Manager.CreatePlugin(pluginsRaw[0])
}
func (c *PluginManagerCatalog) FindPlugins(d Descriptor) ([]Plugin, error) {
filterFn := makeDescriptorFilter(d)
pluginsRaw := make([]*PluginRaw, 0, 10)
c.Manager.Store.Range(func(pluginRaw *PluginRaw) {
if filterFn(&pluginRaw.Metadata) {
pluginsRaw = append(pluginsRaw, pluginRaw)
}
})
results := make([]Plugin, 0, len(pluginsRaw))
errs := []error{}
for _, pr := range pluginsRaw {
p, err := c.Manager.CreatePlugin(pr)
if err != nil {
errs = append(errs, err)
continue
}
results = append(results, p)
}
if err := errors.Join(errs...); err != nil {
return nil, err
}
return results, nil
}
// NewEmptyCatalog returns a Catalog that has no plugins
func NewEmptyCatalog() Catalog {
return &emptyCatalog{}
}
type emptyCatalog struct{}
func (*emptyCatalog) FindPlugin(Descriptor) (Plugin, error) {
return nil, nil
}
func (*emptyCatalog) FindPlugins(Descriptor) ([]Plugin, error) {
return []Plugin{}, nil
}

@ -46,6 +46,20 @@ type MetadataLegacy struct {
Description string `yaml:"description"`
// PlatformCommand is the plugin command, with a platform selector and support for args.
//
// The command and args will be passed through environment expansion, so env vars can
// be present in this command. Unless IgnoreFlags is set, this will
// also merge the flags passed from Helm.
//
// Note that the command is not executed in a shell. To do so, we suggest
// pointing the command to a shell script.
//
// The following rules will apply to processing platform commands:
// - If PlatformCommand is present, it will be used
// - If both OS and Arch match the current platform, search will stop and the command will be executed
// - If OS matches and Arch is empty, the command will be executed
// - If no OS/Arch match is found, the default command will be executed
// - If no matches are found in platformCommand, Helm will exit with an error
PlatformCommand []PlatformCommand `yaml:"platformCommand"`
// Command is the plugin command, as a single string.

@ -15,13 +15,10 @@ package schema
import (
"bytes"
"helm.sh/helm/v4/pkg/cli"
)
type InputMessageCLIV1 struct {
ExtraArgs []string `json:"extraArgs"`
Settings *cli.EnvSettings `json:"settings"`
ExtraArgs []string `json:"extraArgs"`
}
type OutputMessageCLIV1 struct {

@ -25,15 +25,19 @@ package cli
import (
"fmt"
"log/slog"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/spf13/pflag"
"github.com/tetratelabs/wazero"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest"
"helm.sh/helm/v4/internal/plugin"
"helm.sh/helm/v4/internal/version"
"helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v4/pkg/kube"
@ -93,6 +97,8 @@ type EnvSettings struct {
ColorMode string
// ContentCache is the location where cached charts are stored
ContentCache string
// PluginCatalog is the catalog of plugins available
PluginCatalog plugin.Catalog
}
func New() *EnvSettings {
@ -115,6 +121,7 @@ func New() *EnvSettings {
BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit),
QPS: envFloat32Or("HELM_QPS", defaultQPS),
ColorMode: envColorMode(),
PluginCatalog: plugin.NewEmptyCatalog(),
}
env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG"))
@ -297,3 +304,35 @@ func (s *EnvSettings) RESTClientGetter() genericclioptions.RESTClientGetter {
func (s *EnvSettings) ShouldDisableColor() bool {
return s.ColorMode == "never"
}
func (s *EnvSettings) InitializeDefaultPluginManager() error {
// If HELM_NO_PLUGINS is set to 1, do not load plugins.
if os.Getenv("HELM_NO_PLUGINS") != "" {
slog.Debug("HELM_NO_PLUGINS set, skipping plugin initialization")
return nil
}
pm := plugin.NewManager()
// register subprocess runtime
pm.RegisterRuntime("subprocess", &plugin.RuntimeSubprocess{})
// configure and register extism/v1 runtime
wazeroCompilationCache, err := wazero.NewCompilationCacheWithDir(helmpath.CachePath("wazero-build"))
if err != nil {
return fmt.Errorf("failed to create wazero compilation cache: %w", err)
}
pm.RegisterRuntime("extism/v1", &plugin.RuntimeExtismV1{
CompilationCache: wazeroCompilationCache,
})
pluginsDirs := filepath.SplitList(s.PluginsDirectory)
if err := plugin.NewDirLoader(&pm.Store, plugin.GlobPluginDirs).Load(pluginsDirs); err != nil {
return fmt.Errorf("failed to load plugins: %w", err)
}
s.PluginCatalog = pm.Catalog()
return nil
}

@ -24,6 +24,7 @@ import (
"helm.sh/helm/v4/pkg/action"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/cli"
release "helm.sh/helm/v4/pkg/release/v1"
helmtime "helm.sh/helm/v4/pkg/time"
)
@ -101,7 +102,11 @@ func outputFlagCompletionTest(t *testing.T, cmdName string) {
func TestPostRendererFlagSetOnce(t *testing.T) {
cfg := action.Configuration{}
client := action.NewInstall(&cfg)
settings := cli.New()
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
err := settings.InitializeDefaultPluginManager()
require.Nil(t, err)
str := postRendererString{
options: &postRendererOptions{
renderer: &client.PostRenderer,
@ -109,7 +114,7 @@ func TestPostRendererFlagSetOnce(t *testing.T) {
},
}
// Set the plugin name once
err := str.Set("postrenderer-v1")
err = str.Set("postrenderer-v1")
require.NoError(t, err)
// Set the plugin name again to the same value is not ok

@ -21,6 +21,7 @@ import (
"fmt"
"io"
"log"
"log/slog"
"os"
"path/filepath"
"slices"
@ -57,11 +58,10 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) {
return
}
dirs := filepath.SplitList(settings.PluginsDirectory)
descriptor := plugin.Descriptor{
Type: "cli/v1",
}
found, err := plugin.FindPlugins(dirs, descriptor)
found, err := settings.PluginCatalog.FindPlugins(descriptor)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to load plugins: %s\n", err)
return
@ -113,7 +113,6 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) {
input := &plugin.Input{
Message: schema.InputMessageCLIV1{
ExtraArgs: extraArgs,
Settings: settings,
},
Env: env,
Stdin: os.Stdin,
@ -134,6 +133,7 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) {
}
// TODO: Make sure a command with this name does not already exist.
slog.Debug("adding plugin command", "name", c.Name(), "path", plug.Dir())
baseCmd.AddCommand(c)
// For completion, we try to load more details about the plugins so as to allow for command and

@ -16,6 +16,7 @@ limitations under the License.
package cmd
import (
"fmt"
"io"
"github.com/spf13/cobra"
@ -44,8 +45,21 @@ func newPluginCmd(out io.Writer) *cobra.Command {
return cmd
}
// runHook will execute a plugin hook.
func runHook(p plugin.Plugin, event string) error {
// runHook will execute a plugin hook
// currently this function assumes/requires only subprocess plugins can have hooks
func runHook(pm *plugin.Manager, pluginRaw *plugin.PluginRaw, event string) error {
if pluginRaw.Metadata.Runtime != "subprocess" {
return nil
}
pm.Store.Store(pluginRaw)
p, err := pm.CreatePlugin(pluginRaw)
if err != nil {
return fmt.Errorf("plugin is installed but unusable: %w", err)
}
pluginHook, ok := p.(plugin.PluginHook)
if ok {
return pluginHook.InvokeHook(event)

@ -115,7 +115,7 @@ func (o *pluginInstallOptions) newInstallerForSource() (installer.Installer, err
}
// For non-OCI sources, use the original logic
return installer.NewForSource(o.source, o.version)
return installer.NewForSource(settings, o.source, o.version)
}
func (o *pluginInstallOptions) run(out io.Writer) error {
@ -171,12 +171,22 @@ func (o *pluginInstallOptions) run(out io.Writer) error {
}
slog.Debug("loading plugin", "path", i.Path())
p, err := plugin.LoadDir(i.Path())
pluginRaw, err := plugin.LoadDirRaw(i.Path())
pmc, ok := settings.PluginCatalog.(*plugin.PluginManagerCatalog)
if !ok {
return fmt.Errorf("plugin is installed but unusable: %w", err)
}
pm := pmc.Manager
pm.Store.Store(pluginRaw)
p, err := pm.CreatePlugin(pluginRaw)
if err != nil {
return fmt.Errorf("plugin is installed but unusable: %w", err)
}
if err := runHook(p, plugin.Install); err != nil {
if err := runHook(pm, pluginRaw, plugin.Install); err != nil {
return err
}

@ -18,9 +18,8 @@ package cmd
import (
"fmt"
"io"
"log/slog"
"path/filepath"
"slices"
"strings"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
@ -37,12 +36,10 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
Short: "list installed Helm plugins",
ValidArgsFunction: noMoreArgsCompFunc,
RunE: func(_ *cobra.Command, _ []string) error {
slog.Debug("pluginDirs", "directory", settings.PluginsDirectory)
dirs := filepath.SplitList(settings.PluginsDirectory)
descriptor := plugin.Descriptor{
Type: pluginType,
}
plugins, err := plugin.FindPlugins(dirs, descriptor)
plugins, err := settings.PluginCatalog.FindPlugins(descriptor)
if err != nil {
return err
}
@ -97,11 +94,15 @@ func filterPlugins(plugins []plugin.Plugin, ignoredPluginNames []string) []plugi
// Provide dynamic auto-completion for plugin names
func compListPlugins(_ string, ignoredPluginNames []string) []string {
var pNames []string
dirs := filepath.SplitList(settings.PluginsDirectory)
descriptor := plugin.Descriptor{
Type: "cli/v1",
}
plugins, err := plugin.FindPlugins(dirs, descriptor)
plugins, err := settings.PluginCatalog.FindPlugins(descriptor)
slices.SortFunc(plugins, func(i, j plugin.Plugin) int {
return strings.Compare(i.Metadata().Name, j.Metadata().Name)
})
if err == nil && len(plugins) > 0 {
filteredPlugins := filterPlugins(plugins, ignoredPluginNames)
for _, p := range filteredPlugins {

@ -85,7 +85,7 @@ func (o *pluginPackageOptions) run(out io.Writer) error {
}
// Load and validate plugin metadata
pluginMeta, err := plugin.LoadDir(o.pluginPath)
pr, err := plugin.LoadDirRaw(o.pluginPath)
if err != nil {
return fmt.Errorf("invalid plugin directory: %w", err)
}
@ -124,7 +124,7 @@ func (o *pluginPackageOptions) run(out io.Writer) error {
// Now create the tarball (only after signing prerequisites are met)
// Use plugin metadata for filename: PLUGIN_NAME-SEMVER.tgz
metadata := pluginMeta.Metadata()
metadata := pr.Metadata
filename := fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
tarballPath := filepath.Join(o.destination, filename)

@ -87,6 +87,8 @@ func TestLoadCLIPlugins(t *testing.T) {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
settings.RepositoryConfig = "testdata/helmhome/helm/repositories.yaml"
settings.RepositoryCache = "testdata/helmhome/helm/repository"
err := settings.InitializeDefaultPluginManager()
require.Nil(t, err)
var (
out bytes.Buffer
@ -165,6 +167,8 @@ func TestLoadPluginsWithSpace(t *testing.T) {
settings.PluginsDirectory = "testdata/helm home with space/helm/plugins"
settings.RepositoryConfig = "testdata/helm home with space/helm/repositories.yaml"
settings.RepositoryCache = "testdata/helm home with space/helm/repository"
err := settings.InitializeDefaultPluginManager()
require.Nil(t, err)
var (
out bytes.Buffer
@ -243,6 +247,8 @@ type staticCompletionDetails struct {
func TestLoadCLIPluginsForCompletion(t *testing.T) {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
err := settings.InitializeDefaultPluginManager()
require.Nil(t, err)
var out bytes.Buffer
@ -330,15 +336,19 @@ func TestPluginDynamicCompletion(t *testing.T) {
}}
for _, test := range tests {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
err := settings.InitializeDefaultPluginManager()
require.Nil(t, err)
runTestCmd(t, []cmdTestCase{test})
}
}
func TestLoadCLIPlugins_HelmNoPlugins(t *testing.T) {
t.Setenv("HELM_NO_PLUGINS", "1")
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
settings.RepositoryConfig = "testdata/helmhome/helm/repository"
t.Setenv("HELM_NO_PLUGINS", "1")
err := settings.InitializeDefaultPluginManager()
require.Nil(t, err)
out := bytes.NewBuffer(nil)
cmd := &cobra.Command{}
@ -399,6 +409,9 @@ func TestPluginCmdsCompletion(t *testing.T) {
}, {}}
for _, test := range tests {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
err := settings.InitializeDefaultPluginManager()
require.Nil(t, err)
runTestCmd(t, []cmdTestCase{test})
}
}

@ -61,38 +61,43 @@ func (o *pluginUninstallOptions) complete(args []string) error {
}
func (o *pluginUninstallOptions) run(out io.Writer) error {
slog.Debug("loading installer plugins", "dir", settings.PluginsDirectory)
plugins, err := plugin.LoadAll(settings.PluginsDirectory)
if err != nil {
return err
pmc, ok := settings.PluginCatalog.(*plugin.PluginManagerCatalog)
if !ok {
return nil
}
var errorPlugins []error
pm := pmc.Manager
var errs []error
for _, name := range o.names {
if found := findPlugin(plugins, name); found != nil {
if err := uninstallPlugin(found); err != nil {
errorPlugins = append(errorPlugins, fmt.Errorf("failed to uninstall plugin %s, got error (%v)", name, err))
} else {
fmt.Fprintf(out, "Uninstalled plugin: %s\n", name)
}
} else {
errorPlugins = append(errorPlugins, fmt.Errorf("plugin: %s not found", name))
pluginRaw := pm.Store.Load(name)
if pluginRaw == nil {
errs = append(errs, fmt.Errorf("plugin: %s not found", name))
continue
}
if err := uninstallPlugin(pm, pluginRaw); err != nil {
errs = append(errs, fmt.Errorf("failed to uninstall plugin %s, got error (%v)", name, err))
continue
}
fmt.Fprintf(out, "Uninstalled plugin: %s\n", name)
}
if len(errorPlugins) > 0 {
return errors.Join(errorPlugins...)
}
return nil
return errors.Join(errs...)
}
func uninstallPlugin(p plugin.Plugin) error {
if err := os.RemoveAll(p.Dir()); err != nil {
func uninstallPlugin(pm *plugin.Manager, pluginRaw *plugin.PluginRaw) error {
pm.Store.Delete(pluginRaw.Metadata.Name)
if err := os.RemoveAll(pluginRaw.Dir); err != nil {
return err
}
// Clean up versioned tarball and provenance files from HELM_PLUGINS directory
// These files are saved with pattern: PLUGIN_NAME-VERSION.tgz and PLUGIN_NAME-VERSION.tgz.prov
pluginName := p.Metadata().Name
pluginVersion := p.Metadata().Version
pluginName := pluginRaw.Metadata.Name
pluginVersion := pluginRaw.Metadata.Version
pluginsDir := settings.PluginsDirectory
// Remove versioned files: plugin-name-version.tgz and plugin-name-version.tgz.prov
@ -118,15 +123,9 @@ func uninstallPlugin(p plugin.Plugin) error {
}
}
return runHook(p, plugin.Delete)
}
// Ensure a concurrent store reload doesn't accidentally race the os.RemoveAll and read the plugin back into memory
pm.Store.Delete(pluginRaw.Metadata.Name)
// TODO should this be in pkg/plugin/loader.go?
func findPlugin(plugins []plugin.Plugin, name string) plugin.Plugin {
for _, p := range plugins {
if p.Metadata().Name == name {
return p
}
}
return nil
// TODO: should the hook be run before deleting the plugin's files?
return runHook(pm, pluginRaw, plugin.Delete)
}

@ -70,20 +70,20 @@ command: $HELM_PLUGIN_DIR/test-plugin
}
// Load the plugin
p, err := plugin.LoadDir(pluginDir)
pr, err := plugin.LoadDirRaw(pluginDir)
if err != nil {
t.Fatal(err)
}
// Create a test uninstall function that uses our test settings
testUninstallPlugin := func(plugin plugin.Plugin) error {
if err := os.RemoveAll(plugin.Dir()); err != nil {
testUninstallPlugin := func(pluginRaw *plugin.PluginRaw) error {
if err := os.RemoveAll(pluginRaw.Dir); err != nil {
return err
}
// Clean up versioned tarball and provenance files from test HELM_PLUGINS directory
pluginName := plugin.Metadata().Name
pluginVersion := plugin.Metadata().Version
pluginName := pluginRaw.Metadata.Name
pluginVersion := pluginRaw.Metadata.Version
testPluginsDir := testSettings.PluginsDirectory
// Remove versioned files: plugin-name-version.tgz and plugin-name-version.tgz.prov
@ -123,7 +123,7 @@ command: $HELM_PLUGIN_DIR/test-plugin
}
// Uninstall the plugin
if err := testUninstallPlugin(p); err != nil {
if err := testUninstallPlugin(pr); err != nil {
t.Fatal(err)
}

@ -19,7 +19,6 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"path/filepath"
"github.com/spf13/cobra"
@ -61,33 +60,33 @@ func (o *pluginUpdateOptions) complete(args []string) error {
}
func (o *pluginUpdateOptions) run(out io.Writer) error {
installer.Debug = settings.Debug
slog.Debug("loading installed plugins", "path", settings.PluginsDirectory)
plugins, err := plugin.LoadAll(settings.PluginsDirectory)
if err != nil {
return err
pmc, ok := settings.PluginCatalog.(*plugin.PluginManagerCatalog)
if !ok {
return nil
}
var errorPlugins []error
pm := pmc.Manager
var errs []error
for _, name := range o.names {
if found := findPlugin(plugins, name); found != nil {
if err := updatePlugin(found); err != nil {
errorPlugins = append(errorPlugins, fmt.Errorf("failed to update plugin %s, got error (%v)", name, err))
} else {
fmt.Fprintf(out, "Updated plugin: %s\n", name)
}
} else {
errorPlugins = append(errorPlugins, fmt.Errorf("plugin: %s not found", name))
pluginRaw := pm.Store.Load(name)
if pluginRaw == nil {
errs = append(errs, fmt.Errorf("plugin: %s not found", name))
continue
}
if err := updatePlugin(pm, pluginRaw); err != nil {
errs = append(errs, fmt.Errorf("failed to update plugin %s, got error (%v)", name, err))
continue
}
fmt.Fprintf(out, "Uninstalled plugin: %s\n", name)
}
if len(errorPlugins) > 0 {
return errors.Join(errorPlugins...)
}
return nil
return errors.Join(errs...)
}
func updatePlugin(p plugin.Plugin) error {
exactLocation, err := filepath.EvalSymlinks(p.Dir())
func updatePlugin(pm *plugin.Manager, pluginRaw *plugin.PluginRaw) error {
exactLocation, err := filepath.EvalSymlinks(pluginRaw.Dir)
if err != nil {
return err
}
@ -104,11 +103,7 @@ func updatePlugin(p plugin.Plugin) error {
return err
}
slog.Debug("loading plugin", "path", i.Path())
updatedPlugin, err := plugin.LoadDir(i.Path())
if err != nil {
return err
}
pm.Store.Store(pluginRaw)
return runHook(updatedPlugin, plugin.Update)
return runHook(pm, pluginRaw, plugin.Update)
}

@ -102,6 +102,8 @@ By default, the default directories depend on the Operating System. The defaults
var settings = cli.New()
func NewRootCmd(out io.Writer, args []string, logSetup func(bool)) (*cobra.Command, error) {
settings.InitializeDefaultPluginManager()
actionConfig := new(action.Configuration)
cmd, err := newRootCmdWithConfig(actionConfig, out, args, logSetup)
if err != nil {
@ -109,6 +111,7 @@ func NewRootCmd(out io.Writer, args []string, logSetup func(bool)) (*cobra.Comma
}
cobra.OnInitialize(func() {
helmDriver := os.Getenv("HELM_DRIVER")
if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver); err != nil {
log.Fatal(err)
}

@ -24,6 +24,7 @@ import (
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/plugin"
"helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/getter"
@ -79,6 +80,7 @@ func TestResolveChartRef(t *testing.T) {
Getters: getter.All(&cli.EnvSettings{
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
PluginCatalog: plugin.NewEmptyCatalog(),
}),
}
@ -119,6 +121,7 @@ func TestResolveChartOpts(t *testing.T) {
Getters: getter.All(&cli.EnvSettings{
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
PluginCatalog: plugin.NewEmptyCatalog(),
}),
}
@ -215,6 +218,7 @@ func TestDownloadTo(t *testing.T) {
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
ContentCache: contentCache,
PluginCatalog: plugin.NewEmptyCatalog(),
}),
Options: []getter.Option{
getter.WithBasicAuth("username", "password"),
@ -271,6 +275,7 @@ func TestDownloadTo_TLS(t *testing.T) {
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
ContentCache: contentCache,
PluginCatalog: plugin.NewEmptyCatalog(),
}),
Options: []getter.Option{
getter.WithTLSClientConfig(
@ -327,6 +332,7 @@ func TestDownloadTo_VerifyLater(t *testing.T) {
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
ContentCache: contentCache,
PluginCatalog: plugin.NewEmptyCatalog(),
}),
}
cname := "/signtest-0.1.0.tgz"
@ -356,6 +362,7 @@ func TestScanReposForURL(t *testing.T) {
Getters: getter.All(&cli.EnvSettings{
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
PluginCatalog: plugin.NewEmptyCatalog(),
}),
}

@ -19,6 +19,8 @@ import (
"testing"
"time"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/cli"
)
@ -73,6 +75,8 @@ func TestProvidersWithTimeout(t *testing.T) {
func TestAll(t *testing.T) {
env := cli.New()
env.PluginsDirectory = pluginDir
err := env.InitializeDefaultPluginManager()
require.Nil(t, err)
all := All(env)
if len(all) != 4 {
@ -87,6 +91,8 @@ func TestAll(t *testing.T) {
func TestByScheme(t *testing.T) {
env := cli.New()
env.PluginsDirectory = pluginDir
err := env.InitializeDefaultPluginManager()
require.Nil(t, err)
g := All(env)
if _, err := g.ByScheme("test"); err != nil {

@ -34,7 +34,7 @@ func collectGetterPlugins(settings *cli.EnvSettings) (Providers, error) {
d := plugin.Descriptor{
Type: "getter/v1",
}
plgs, err := plugin.FindPlugins([]string{settings.PluginsDirectory}, d)
plgs, err := settings.PluginCatalog.FindPlugins(d)
if err != nil {
return nil, err
}

@ -33,6 +33,8 @@ import (
func TestCollectPlugins(t *testing.T) {
env := cli.New()
env.PluginsDirectory = pluginDir
err := env.InitializeDefaultPluginManager()
require.Nil(t, err)
p, err := collectGetterPlugins(env)
if err != nil {

@ -17,7 +17,6 @@ import (
"bytes"
"context"
"fmt"
"path/filepath"
"helm.sh/helm/v4/internal/plugin/schema"
@ -40,7 +39,7 @@ func NewPostRendererPlugin(settings *cli.EnvSettings, pluginName string, args ..
Name: pluginName,
Type: "postrenderer/v1",
}
p, err := plugin.FindPlugin(filepath.SplitList(settings.PluginsDirectory), descriptor)
p, err := settings.PluginCatalog.FindPlugin(descriptor)
if err != nil {
return nil, err
}

@ -29,6 +29,7 @@ import (
"sigs.k8s.io/yaml"
"helm.sh/helm/v4/internal/plugin"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/getter"
)
@ -131,7 +132,9 @@ func TestFindChartInAuthAndTLSAndPassRepoURL(t *testing.T) {
chartURL, err := FindChartInRepoURL(
srv.URL,
"nginx",
getter.All(&cli.EnvSettings{}),
getter.All(&cli.EnvSettings{
PluginCatalog: plugin.NewEmptyCatalog(),
}),
WithInsecureSkipTLSverify(true),
)
if err != nil {
@ -142,7 +145,7 @@ func TestFindChartInAuthAndTLSAndPassRepoURL(t *testing.T) {
}
// If the insecureSkipTLSVerify is false, it will return an error that contains "x509: certificate signed by unknown authority".
_, err = FindChartInRepoURL(srv.URL, "nginx", getter.All(&cli.EnvSettings{}), WithChartVersion("0.1.0"))
_, err = FindChartInRepoURL(srv.URL, "nginx", getter.All(&cli.EnvSettings{PluginCatalog: plugin.NewEmptyCatalog()}), WithChartVersion("0.1.0"))
// Go communicates with the platform and different platforms return different messages. Go itself tests darwin
// differently for its message. On newer versions of Darwin the message includes the "Acme Co" portion while older
// versions of Darwin do not. As there are people developing Helm using both old and new versions of Darwin we test
@ -163,7 +166,7 @@ func TestFindChartInRepoURL(t *testing.T) {
}
defer srv.Close()
chartURL, err := FindChartInRepoURL(srv.URL, "nginx", getter.All(&cli.EnvSettings{}))
chartURL, err := FindChartInRepoURL(srv.URL, "nginx", getter.All(&cli.EnvSettings{PluginCatalog: plugin.NewEmptyCatalog()}))
if err != nil {
t.Fatalf("%v", err)
}
@ -171,7 +174,7 @@ func TestFindChartInRepoURL(t *testing.T) {
t.Errorf("%s is not the valid URL", chartURL)
}
chartURL, err = FindChartInRepoURL(srv.URL, "nginx", getter.All(&cli.EnvSettings{}), WithChartVersion("0.1.0"))
chartURL, err = FindChartInRepoURL(srv.URL, "nginx", getter.All(&cli.EnvSettings{PluginCatalog: plugin.NewEmptyCatalog()}), WithChartVersion("0.1.0"))
if err != nil {
t.Errorf("%s", err)
}
@ -184,6 +187,7 @@ func TestErrorFindChartInRepoURL(t *testing.T) {
g := getter.All(&cli.EnvSettings{
RepositoryCache: t.TempDir(),
PluginCatalog: plugin.NewEmptyCatalog(),
})
if _, err := FindChartInRepoURL("http://someserver/something", "nginx", g); err == nil {

@ -28,6 +28,7 @@ import (
"strings"
"testing"
"helm.sh/helm/v4/internal/plugin"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/getter"
@ -264,7 +265,7 @@ func TestDownloadIndexFile(t *testing.T) {
r, err := NewChartRepository(&Entry{
Name: testRepo,
URL: srv.URL,
}, getter.All(&cli.EnvSettings{}))
}, getter.All(&cli.EnvSettings{PluginCatalog: plugin.NewEmptyCatalog()}))
if err != nil {
t.Errorf("Problem creating chart repository from %s: %v", testRepo, err)
}
@ -317,7 +318,7 @@ func TestDownloadIndexFile(t *testing.T) {
r, err := NewChartRepository(&Entry{
Name: testRepo,
URL: srv.URL + chartRepoURLPath,
}, getter.All(&cli.EnvSettings{}))
}, getter.All(&cli.EnvSettings{PluginCatalog: plugin.NewEmptyCatalog()}))
if err != nil {
t.Errorf("Problem creating chart repository from %s: %v", testRepo, err)
}

Loading…
Cancel
Save