diff --git a/internal/plugin/installer/base.go b/internal/plugin/installer/base.go index 4ef7f2b29..1b2f5c8a9 100644 --- a/internal/plugin/installer/base.go +++ b/internal/plugin/installer/base.go @@ -30,11 +30,16 @@ type base struct { func newBase(source string) base { settings := cli.New() - // When HELM_PLUGINS contains a list of paths, use only the first one for - // installation so the plugin ends up in a directory that is actually searched. + // When HELM_PLUGINS contains a list of paths, install into the first non-empty + // entry so the plugin ends up in a directory that is actually searched. Skipping + // empty segments avoids installing into a relative path when HELM_PLUGINS contains + // entries like ":/path" or "/path:". pluginsDir := settings.PluginsDirectory - if dirs := filepath.SplitList(pluginsDir); len(dirs) > 0 { - pluginsDir = dirs[0] + for _, dir := range filepath.SplitList(pluginsDir) { + if dir != "" { + pluginsDir = dir + break + } } return base{ Source: source, diff --git a/internal/plugin/installer/base_test.go b/internal/plugin/installer/base_test.go index 8a1056605..9f6ade8fd 100644 --- a/internal/plugin/installer/base_test.go +++ b/internal/plugin/installer/base_test.go @@ -66,6 +66,22 @@ func TestPathEmptyPluginDir(t *testing.T) { // When HELM_PLUGINS is explicitly empty, newBase must not panic. t.Setenv("HELM_PLUGINS", "") b := newBase("https://github.com/jkroepke/helm-secrets") - // Path() returns "" when source is "" or PluginsDirectory is ""; just verify no panic. + // Path() only returns "" when Source is ""; with an empty PluginsDirectory it + // returns a relative path. Just verify no panic. _ = b.Path() } + +func TestPathSkipsEmptyPluginDirs(t *testing.T) { + // A leading empty segment (e.g. ":/real/path") must be skipped so the plugin is + // installed into the first real directory rather than a relative path. + real := filepath.FromSlash("/helm/data/plugins") + multiPath := string(filepath.ListSeparator) + real + + t.Setenv("HELM_PLUGINS", multiPath) + b := newBase("https://github.com/jkroepke/helm-secrets") + got := b.Path() + expected := filepath.Join(real, "helm-secrets") + if got != expected { + t.Errorf("expected path %s, got %s", expected, got) + } +} diff --git a/internal/plugin/installer/http_installer_test.go b/internal/plugin/installer/http_installer_test.go index a5fdfb399..a43f4d9d2 100644 --- a/internal/plugin/installer/http_installer_test.go +++ b/internal/plugin/installer/http_installer_test.go @@ -1,3 +1,5 @@ +//go:build !windows + /* Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,8 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -//go:build !windows - package installer // import "helm.sh/helm/v4/internal/plugin/installer" import ( diff --git a/internal/plugin/installer/oci_installer.go b/internal/plugin/installer/oci_installer.go index 0a522e555..f323c7aa7 100644 --- a/internal/plugin/installer/oci_installer.go +++ b/internal/plugin/installer/oci_installer.go @@ -190,7 +190,7 @@ func (i OCIInstaller) Path() string { if i.Source == "" { return "" } - return filepath.Join(i.base.PluginsDirectory, i.PluginName) + return filepath.Join(i.PluginsDirectory, i.PluginName) } // extractTarGz extracts a gzipped tar archive to a directory