From c3c7c322b5119d555fe75c53c3b632eb75054319 Mon Sep 17 00:00:00 2001 From: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:05:41 +0800 Subject: [PATCH 1/9] fix(plugin): install plugins into the first HELM_PLUGINS directory When HELM_PLUGINS contains a list of directories, plugins should be installed into the first directory. The installer previously used the raw value as a single directory, which failed when a list was configured. Fixes #11310 Signed-off-by: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> --- internal/plugin/installer/base.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/plugin/installer/base.go b/internal/plugin/installer/base.go index 54d25287a..2e4c16f4a 100644 --- a/internal/plugin/installer/base.go +++ b/internal/plugin/installer/base.go @@ -36,10 +36,21 @@ func newBase(source string) base { } } +// pluginsDir returns the directory where plugins should be installed. When +// HELM_PLUGINS contains a list of directories (separated by the OS path list +// separator), the first directory is the default install target. +func (b *base) pluginsDir() string { + dirs := filepath.SplitList(b.PluginsDirectory) + if len(dirs) == 0 { + return b.PluginsDirectory + } + return dirs[0] +} + // Path is where the plugin will be installed. func (b *base) Path() string { if b.Source == "" { return "" } - return filepath.Join(b.PluginsDirectory, filepath.Base(b.Source)) + return filepath.Join(b.pluginsDir(), filepath.Base(b.Source)) } From 96a9f07916d9a29d17d8a1794f3f31aaca2b6326 Mon Sep 17 00:00:00 2001 From: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:06:23 +0800 Subject: [PATCH 2/9] test(plugin): cover install into first HELM_PLUGINS directory Adds a TestPath case where HELM_PLUGINS contains multiple directories and verifies the plugin installs into the first one. Signed-off-by: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> --- internal/plugin/installer/base_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/plugin/installer/base_test.go b/internal/plugin/installer/base_test.go index 8ef7ff588..f27be4037 100644 --- a/internal/plugin/installer/base_test.go +++ b/internal/plugin/installer/base_test.go @@ -14,6 +14,8 @@ limitations under the License. package installer import ( + "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -33,6 +35,10 @@ func TestPath(t *testing.T) { source: "https://github.com/jkroepke/helm-secrets", helmPluginsDir: "/helm/data/plugins", expectPath: "/helm/data/plugins/helm-secrets", + }, { + source: "https://github.com/jkroepke/helm-secrets", + helmPluginsDir: strings.Join([]string{"/helm/data/plugins", "/helm/data/extra"}, string(filepath.ListSeparator)), + expectPath: "/helm/data/plugins/helm-secrets", }, } From 5926d700ec94db6f5875267415eda9f44a38dcb7 Mon Sep 17 00:00:00 2001 From: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:52:23 +0800 Subject: [PATCH 3/9] fix(plugin): apply first HELM_PLUGINS directory to OCI installs OCI installs should also target the first directory in HELM_PLUGINS when a list is configured, matching base.Path(). Signed-off-by: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> --- internal/plugin/installer/oci_installer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/plugin/installer/oci_installer.go b/internal/plugin/installer/oci_installer.go index 383ddb914..9b62dc980 100644 --- a/internal/plugin/installer/oci_installer.go +++ b/internal/plugin/installer/oci_installer.go @@ -195,7 +195,7 @@ func (i OCIInstaller) Path() string { if i.Source == "" { return "" } - return filepath.Join(i.settings.PluginsDirectory, i.PluginName) + return filepath.Join(i.pluginsDir(), i.PluginName) } // extractTarGz extracts a gzipped tar archive to a directory From bd017324eb6c731eb481646fd5724f4502a9c700 Mon Sep 17 00:00:00 2001 From: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:58:26 +0800 Subject: [PATCH 4/9] fix(plugin): skip empty entries when resolving plugins dir Guard against leading/trailing/duplicate list separators in HELM_PLUGINS so pluginsDir never resolves to an empty path. Signed-off-by: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> --- internal/plugin/installer/base.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/plugin/installer/base.go b/internal/plugin/installer/base.go index 2e4c16f4a..361abff05 100644 --- a/internal/plugin/installer/base.go +++ b/internal/plugin/installer/base.go @@ -40,11 +40,12 @@ func newBase(source string) base { // HELM_PLUGINS contains a list of directories (separated by the OS path list // separator), the first directory is the default install target. func (b *base) pluginsDir() string { - dirs := filepath.SplitList(b.PluginsDirectory) - if len(dirs) == 0 { - return b.PluginsDirectory + for _, dir := range filepath.SplitList(b.PluginsDirectory) { + if dir != "" { + return dir + } } - return dirs[0] + return b.PluginsDirectory } // Path is where the plugin will be installed. From 5a2ccbd2d4ce6669ebacbd418aa7ec46e6381a73 Mon Sep 17 00:00:00 2001 From: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:59:03 +0800 Subject: [PATCH 5/9] test(plugin): cover leading empty entry in HELM_PLUGINS Adds a TestPath case where the list begins with the separator so the first non-empty directory is chosen. Signed-off-by: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> --- internal/plugin/installer/base_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/plugin/installer/base_test.go b/internal/plugin/installer/base_test.go index f27be4037..483f49299 100644 --- a/internal/plugin/installer/base_test.go +++ b/internal/plugin/installer/base_test.go @@ -39,6 +39,10 @@ func TestPath(t *testing.T) { source: "https://github.com/jkroepke/helm-secrets", helmPluginsDir: strings.Join([]string{"/helm/data/plugins", "/helm/data/extra"}, string(filepath.ListSeparator)), expectPath: "/helm/data/plugins/helm-secrets", + }, { + source: "https://github.com/jkroepke/helm-secrets", + helmPluginsDir: string(filepath.ListSeparator) + "/helm/data/plugins", + expectPath: "/helm/data/plugins/helm-secrets", }, } From 6e0a63e81c262cdd142464f7efb4829d87b695a4 Mon Sep 17 00:00:00 2001 From: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:52:29 +0800 Subject: [PATCH 6/9] fix(plugin): use HELM_PLUGINS dir for HTTP plugin installs Make HTTPInstaller.Path() use the first HELM_PLUGINS directory, matching base.Path() and OCI. Signed-off-by: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> --- internal/plugin/installer/http_installer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/plugin/installer/http_installer.go b/internal/plugin/installer/http_installer.go index 7be326de2..3fbe6a3e1 100644 --- a/internal/plugin/installer/http_installer.go +++ b/internal/plugin/installer/http_installer.go @@ -152,7 +152,7 @@ func (i HTTPInstaller) Path() string { if i.Source == "" { return "" } - return helmpath.DataPath("plugins", i.PluginName) + return filepath.Join(i.pluginsDir(), i.PluginName) } // SupportsVerification returns true if the HTTP installer can verify plugins From 256c28145ed00b76417f98abcf76763388589dc4 Mon Sep 17 00:00:00 2001 From: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:53:08 +0800 Subject: [PATCH 7/9] fix(plugin): use HELM_PLUGINS dir for local plugin installs Make LocalInstaller.Path() use the first HELM_PLUGINS directory, matching base.Path() and OCI. Signed-off-by: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> --- internal/plugin/installer/local_installer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/plugin/installer/local_installer.go b/internal/plugin/installer/local_installer.go index 59918401b..1505d0f57 100644 --- a/internal/plugin/installer/local_installer.go +++ b/internal/plugin/installer/local_installer.go @@ -176,7 +176,7 @@ func (i *LocalInstaller) Path() string { pluginName = stripPluginName(pluginName) } - return helmpath.DataPath("plugins", pluginName) + return filepath.Join(i.pluginsDir(), pluginName) } // SupportsVerification returns true if the local installer can verify plugins From 85b60705a26d4a0326fcd525905c1cbb7a8f8f44 Mon Sep 17 00:00:00 2001 From: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:53:51 +0800 Subject: [PATCH 8/9] test(plugin): make expected paths OS-agnostic in installer tests Build expected paths with filepath.Join so the TestPath assertions pass on Windows as well as Unix. Signed-off-by: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> --- internal/plugin/installer/base_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/plugin/installer/base_test.go b/internal/plugin/installer/base_test.go index 483f49299..0f1b74160 100644 --- a/internal/plugin/installer/base_test.go +++ b/internal/plugin/installer/base_test.go @@ -34,15 +34,15 @@ func TestPath(t *testing.T) { }, { source: "https://github.com/jkroepke/helm-secrets", helmPluginsDir: "/helm/data/plugins", - expectPath: "/helm/data/plugins/helm-secrets", + expectPath: filepath.Join("/helm/data/plugins", "helm-secrets"), }, { source: "https://github.com/jkroepke/helm-secrets", helmPluginsDir: strings.Join([]string{"/helm/data/plugins", "/helm/data/extra"}, string(filepath.ListSeparator)), - expectPath: "/helm/data/plugins/helm-secrets", + expectPath: filepath.Join("/helm/data/plugins", "helm-secrets"), }, { source: "https://github.com/jkroepke/helm-secrets", helmPluginsDir: string(filepath.ListSeparator) + "/helm/data/plugins", - expectPath: "/helm/data/plugins/helm-secrets", + expectPath: filepath.Join("/helm/data/plugins", "helm-secrets"), }, } From 62857cf906fc1e92bf92d94980cd41c7d45d2cf9 Mon Sep 17 00:00:00 2001 From: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:05:23 +0800 Subject: [PATCH 9/9] docs(plugin): clarify pluginsDir comment Update the comment to state that the first non-empty directory in HELM_PLUGINS is used. Signed-off-by: Yuhui Luo <92632263+lyh7c00@users.noreply.github.com> --- internal/plugin/installer/base.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/plugin/installer/base.go b/internal/plugin/installer/base.go index 361abff05..5f8e745cc 100644 --- a/internal/plugin/installer/base.go +++ b/internal/plugin/installer/base.go @@ -38,7 +38,7 @@ func newBase(source string) base { // pluginsDir returns the directory where plugins should be installed. When // HELM_PLUGINS contains a list of directories (separated by the OS path list -// separator), the first directory is the default install target. +// separator), the first non-empty directory is the default install target. func (b *base) pluginsDir() string { for _, dir := range filepath.SplitList(b.PluginsDirectory) { if dir != "" {