From 0a30a95941b9e5069b7bb8cb570aad09c013dc77 Mon Sep 17 00:00:00 2001 From: Florian Rey Date: Fri, 23 Jan 2026 09:57:57 +0100 Subject: [PATCH] fix: honor plugins custom directory for http and local installers Signed-off-by: Florian Rey --- internal/plugin/installer/base_test.go | 42 +++++++++++----- internal/plugin/installer/http_installer.go | 6 ++- .../plugin/installer/http_installer_test.go | 37 ++++++++++++++ internal/plugin/installer/local_installer.go | 10 +++- .../plugin/installer/local_installer_test.go | 47 ++++++++++++++++++ .../plugin/installer/oci_installer_test.go | 39 +++++++-------- .../plugin/installer/vcs_installer_test.go | 40 +++++++++++++++ .../plugdir/good/archive-1.2.3.tar.gz | Bin 0 -> 474 bytes 8 files changed, 186 insertions(+), 35 deletions(-) create mode 100644 internal/plugin/testdata/plugdir/good/archive-1.2.3.tar.gz diff --git a/internal/plugin/installer/base_test.go b/internal/plugin/installer/base_test.go index 62b77bde5..396b201ec 100644 --- a/internal/plugin/installer/base_test.go +++ b/internal/plugin/installer/base_test.go @@ -15,32 +15,50 @@ package installer // import "helm.sh/helm/v4/internal/plugin/installer" import ( "testing" + + "helm.sh/helm/v4/pkg/helmpath" ) -func TestPath(t *testing.T) { +func Test_Path(t *testing.T) { tests := []struct { + name string source string helmPluginsDir string expectPath string }{ { + name: "empty source default helm plugins dir", + source: "", + helmPluginsDir: "", + expectPath: "", + }, { + name: "default helm plugins dir", + source: "https://github.com/adamreese/helm-env", + helmPluginsDir: "", + expectPath: helmpath.DataPath("plugins", "helm-env"), + }, { + name: "empty source custom helm plugins dir", source: "", - helmPluginsDir: "/helm/data/plugins", + helmPluginsDir: "/foo/bar", expectPath: "", }, { - source: "https://github.com/jkroepke/helm-secrets", - helmPluginsDir: "/helm/data/plugins", - expectPath: "/helm/data/plugins/helm-secrets", + name: "custom helm plugins dir", + source: "https://github.com/adamreese/helm-env", + helmPluginsDir: "/foo/bar", + expectPath: "/foo/bar/helm-env", }, } for _, tt := range tests { - - t.Setenv("HELM_PLUGINS", tt.helmPluginsDir) - baseIns := newBase(tt.source) - baseInsPath := baseIns.Path() - if baseInsPath != tt.expectPath { - t.Errorf("expected name %s, got %s", tt.expectPath, baseInsPath) - } + t.Run(tt.name, func(t *testing.T) { + if tt.helmPluginsDir != "" { + t.Setenv("HELM_PLUGINS", tt.helmPluginsDir) + } + installer := newBase(tt.source) + path := installer.Path() + if path != tt.expectPath { + t.Errorf("expected path %s, got %s", tt.expectPath, path) + } + }) } } diff --git a/internal/plugin/installer/http_installer.go b/internal/plugin/installer/http_installer.go index bb96314f4..73ee1a75d 100644 --- a/internal/plugin/installer/http_installer.go +++ b/internal/plugin/installer/http_installer.go @@ -36,6 +36,7 @@ type HTTPInstaller struct { CacheDir string PluginName string base + settings *cli.EnvSettings extractor Extractor getter getter.Getter // Cached data to avoid duplicate downloads @@ -50,6 +51,8 @@ func NewHTTPInstaller(source string) (*HTTPInstaller, error) { return nil, err } + settings := cli.New() + extractor, err := NewExtractor(source) if err != nil { return nil, err @@ -64,6 +67,7 @@ func NewHTTPInstaller(source string) (*HTTPInstaller, error) { CacheDir: helmpath.CachePath("plugins", key), PluginName: stripPluginName(filepath.Base(source)), base: newBase(source), + settings: settings, extractor: extractor, getter: get, } @@ -151,7 +155,7 @@ func (i HTTPInstaller) Path() string { if i.Source == "" { return "" } - return helmpath.DataPath("plugins", i.PluginName) + return filepath.Join(i.settings.PluginsDirectory, i.PluginName) } // SupportsVerification returns true if the HTTP installer can verify plugins diff --git a/internal/plugin/installer/http_installer_test.go b/internal/plugin/installer/http_installer_test.go index 7f7e6cef6..22271493b 100644 --- a/internal/plugin/installer/http_installer_test.go +++ b/internal/plugin/installer/http_installer_test.go @@ -599,3 +599,40 @@ func TestExtractPluginInSubdirectory(t *testing.T) { t.Errorf("Expected plugin root to be %s but got %s", expectedRoot, pluginRoot) } } + +func TestHttpInstaller_Path(t *testing.T) { + tests := []struct { + name string + source string + helmPluginsDir string + expectPath string + }{ + { + name: "default helm plugins dir", + source: "https://example.com/fake-plugin-0.0.1.tar.gz", + helmPluginsDir: "", + expectPath: helmpath.DataPath("plugins", "fake-plugin"), + }, { + name: "custom helm plugins dir", + source: "https://example.com/fake-plugin-0.0.1.tar.gz", + helmPluginsDir: "/foo/bar", + expectPath: "/foo/bar/fake-plugin", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.helmPluginsDir != "" { + t.Setenv("HELM_PLUGINS", tt.helmPluginsDir) + } + installer, err := NewHTTPInstaller(tt.source) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + path := installer.Path() + if path != tt.expectPath { + t.Errorf("expected path %s, got %s", tt.expectPath, path) + } + }) + } +} diff --git a/internal/plugin/installer/local_installer.go b/internal/plugin/installer/local_installer.go index 1c8314282..05fc94967 100644 --- a/internal/plugin/installer/local_installer.go +++ b/internal/plugin/installer/local_installer.go @@ -26,6 +26,7 @@ import ( "helm.sh/helm/v4/internal/plugin" "helm.sh/helm/v4/internal/third_party/dep/fs" + "helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/helmpath" ) @@ -35,6 +36,7 @@ var ErrPluginNotADirectory = errors.New("expected plugin to be a directory (cont // LocalInstaller installs plugins from the filesystem. type LocalInstaller struct { base + settings *cli.EnvSettings isArchive bool extractor Extractor pluginData []byte // Cached plugin data @@ -47,8 +49,12 @@ func NewLocalInstaller(source string) (*LocalInstaller, error) { if err != nil { return nil, fmt.Errorf("unable to get absolute path to plugin: %w", err) } + + settings := cli.New() + i := &LocalInstaller{ - base: newBase(src), + base: newBase(src), + settings: settings, } // Check if source is an archive @@ -176,7 +182,7 @@ func (i *LocalInstaller) Path() string { pluginName = stripPluginName(pluginName) } - return helmpath.DataPath("plugins", pluginName) + return filepath.Join(i.settings.PluginsDirectory, pluginName) } // SupportsVerification returns true if the local installer can verify plugins diff --git a/internal/plugin/installer/local_installer_test.go b/internal/plugin/installer/local_installer_test.go index 3ee8ab6d0..3a33cc56e 100644 --- a/internal/plugin/installer/local_installer_test.go +++ b/internal/plugin/installer/local_installer_test.go @@ -146,3 +146,50 @@ func TestLocalInstallerTarball(t *testing.T) { t.Fatalf("plugin not found at %s: %v", i.Path(), err) } } + +func TestLocalInstaller_Path(t *testing.T) { + tests := []struct { + name string + source string + helmPluginsDir string + expectPath string + }{ + { + name: "default helm plugins dir", + source: "../testdata/plugdir/good/echo-v1", + helmPluginsDir: "", + expectPath: helmpath.DataPath("plugins", "echo-v1"), + }, { + name: "archive default helm plugins dir", + source: "../testdata/plugdir/good/archive-1.2.3.tar.gz", + helmPluginsDir: "", + expectPath: helmpath.DataPath("plugins", "archive"), + }, { + name: "custom helm plugins dir", + source: "../testdata/plugdir/good/echo-v1", + helmPluginsDir: "/foo/bar", + expectPath: "/foo/bar/echo-v1", + }, { + name: "archive custom helm plugins dir", + source: "../testdata/plugdir/good/archive-1.2.3.tar.gz", + helmPluginsDir: "/foo/bar", + expectPath: "/foo/bar/archive", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.helmPluginsDir != "" { + t.Setenv("HELM_PLUGINS", tt.helmPluginsDir) + } + installer, err := NewLocalInstaller(tt.source) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + path := installer.Path() + if path != tt.expectPath { + t.Errorf("expected path %s, got %s", tt.expectPath, path) + } + }) + } +} diff --git a/internal/plugin/installer/oci_installer_test.go b/internal/plugin/installer/oci_installer_test.go index 1280cf97d..358f82c4e 100644 --- a/internal/plugin/installer/oci_installer_test.go +++ b/internal/plugin/installer/oci_installer_test.go @@ -35,7 +35,6 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "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" ) @@ -281,33 +280,33 @@ func TestNewOCIInstaller(t *testing.T) { func TestOCIInstaller_Path(t *testing.T) { tests := []struct { - name string - source string - pluginName string - expectPath string + name string + source string + helmPluginsDir string + expectPath string }{ { - name: "valid plugin name", - source: "oci://ghcr.io/user/plugin-name:v1.0.0", - pluginName: "plugin-name", - expectPath: helmpath.DataPath("plugins", "plugin-name"), - }, - { - name: "empty source", - source: "", - pluginName: "", - expectPath: "", + name: "default helm plugins dir", + source: "oci://ghcr.io/user/plugin-name:v1.0.0", + helmPluginsDir: "", + expectPath: helmpath.DataPath("plugins", "plugin-name"), + }, { + name: "custom helm plugins dir", + source: "oci://ghcr.io/user/plugin-name:v1.0.0", + helmPluginsDir: "/foo/bar", + expectPath: "/foo/bar/plugin-name", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - installer := &OCIInstaller{ - PluginName: tt.pluginName, - base: newBase(tt.source), - settings: cli.New(), + if tt.helmPluginsDir != "" { + t.Setenv("HELM_PLUGINS", tt.helmPluginsDir) + } + installer, err := NewOCIInstaller(tt.source) + if err != nil { + t.Fatalf("unexpected error: %v", err) } - path := installer.Path() if path != tt.expectPath { t.Errorf("expected path %s, got %s", tt.expectPath, path) diff --git a/internal/plugin/installer/vcs_installer_test.go b/internal/plugin/installer/vcs_installer_test.go index d542a0f75..4c1b6be97 100644 --- a/internal/plugin/installer/vcs_installer_test.go +++ b/internal/plugin/installer/vcs_installer_test.go @@ -187,3 +187,43 @@ func TestVCSInstallerUpdate(t *testing.T) { } } + +func TestVCSInstaller_Path(t *testing.T) { + tests := []struct { + name string + source string + version string + helmPluginsDir string + expectPath string + }{ + { + name: "default helm plugins dir", + source: "https://github.com/adamreese/helm-env", + version: "0.2.0", + helmPluginsDir: "", + expectPath: helmpath.DataPath("plugins", "helm-env"), + }, { + name: "custom helm plugins dir", + source: "https://github.com/adamreese/helm-env", + version: "0.2.0", + helmPluginsDir: "/foo/bar", + expectPath: "/foo/bar/helm-env", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.helmPluginsDir != "" { + t.Setenv("HELM_PLUGINS", tt.helmPluginsDir) + } + installer, err := NewVCSInstaller(tt.source, tt.version) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + path := installer.Path() + if path != tt.expectPath { + t.Errorf("expected path %s, got %s", tt.expectPath, path) + } + }) + } +} diff --git a/internal/plugin/testdata/plugdir/good/archive-1.2.3.tar.gz b/internal/plugin/testdata/plugdir/good/archive-1.2.3.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..3082f4b06842708f16b0fcee3b0629de998b63b1 GIT binary patch literal 474 zcmV<00VVz)iwFP^K6hyV1MQYgOT#c2$D<%(J?lB_zL@to!NU|`1K$-052W|zo@C}oK)RBtP@>+5S}zmnkPyBc^-78WB;m=7*HG-VAyt)@Y(I7&>YL*Ia@ zY4E&|sMCkX%MmQn@g2LmQ#1P9#7o8914H#sa8CbRDhgny0AD^a;reG72JJ>*R*8}o zPAL`=v5Hby3d%j9;AVZTy1!nlA8nwHZntCfm0VY=d-ZCxc0{+yP3@Zc9*$C0S4w@sL!|sRV z{73npP?Xy97d$Hq)$ literal 0 HcmV?d00001