From d73846c93f305a946067f36ea888f82418cfd03d 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 | 40 +++++++++++---- internal/plugin/installer/http_installer.go | 6 ++- .../plugin/installer/http_installer_test.go | 39 +++++++++++++++ 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, 188 insertions(+), 33 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 6df8ec8a1..f19c4e505 100644 --- a/internal/plugin/installer/base_test.go +++ b/internal/plugin/installer/base_test.go @@ -14,32 +14,52 @@ limitations under the License. package installer // import "helm.sh/helm/v4/internal/plugin/installer" import ( + "path/filepath" "testing" + + "helm.sh/helm/v4/pkg/helmpath" ) func TestPath(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: filepath.Join("/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 5a2912d2e..ea3f8cd1a 100644 --- a/internal/plugin/installer/http_installer.go +++ b/internal/plugin/installer/http_installer.go @@ -37,6 +37,7 @@ type HTTPInstaller struct { CacheDir string PluginName string base + settings *cli.EnvSettings extractor Extractor getter getter.Getter // Cached data to avoid duplicate downloads @@ -51,6 +52,8 @@ func NewHTTPInstaller(source string) (*HTTPInstaller, error) { return nil, err } + settings := cli.New() + extractor, err := NewExtractor(source) if err != nil { return nil, err @@ -65,6 +68,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, } @@ -152,7 +156,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 efbca90c9..9b465aca0 100644 --- a/internal/plugin/installer/http_installer_test.go +++ b/internal/plugin/installer/http_installer_test.go @@ -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" ) @@ -562,6 +563,7 @@ func TestExtractPluginInSubdirectory(t *testing.T) { CacheDir: tempDir, PluginName: "subdir-plugin", base: newBase(source), + settings: cli.New(), extractor: &TarGzExtractor{}, } @@ -594,3 +596,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: filepath.Join("/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 71407380f..bcbd25166 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 510c2880b..45d73f039 100644 --- a/internal/plugin/installer/local_installer_test.go +++ b/internal/plugin/installer/local_installer_test.go @@ -147,3 +147,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: filepath.Join("/foo/bar", "echo-v1"), + }, { + name: "archive custom helm plugins dir", + source: "../testdata/plugdir/good/archive-1.2.3.tar.gz", + helmPluginsDir: "/foo/bar", + expectPath: filepath.Join("/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 1f25f4e76..02f866b0d 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: filepath.Join("/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 54f94c724..6e2c710a0 100644 --- a/internal/plugin/installer/vcs_installer_test.go +++ b/internal/plugin/installer/vcs_installer_test.go @@ -186,3 +186,43 @@ func TestVCSInstallerUpdate(t *testing.T) { t.Fatalf("expected error for plugin modified, got (%v)", err) } } + +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: filepath.Join("/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