fix(internal): LocalInstaller fails when archive name does not follow convention

Signed-off-by: Niklas Seyfarth <niklas@seyfarth.de>
pull/32638/head
Niklas Seyfarth 1 day ago
parent 87268ee077
commit 189fede18b
No known key found for this signature in database
GPG Key ID: 817F7A9E79723DF9

@ -39,6 +39,7 @@ type LocalInstaller struct {
extractor Extractor extractor Extractor
pluginData []byte // Cached plugin data pluginData []byte // Cached plugin data
provData []byte // Cached provenance data provData []byte // Cached provenance data
pluginName string // Cached plugin name
} }
// NewLocalInstaller creates a new LocalInstaller. // NewLocalInstaller creates a new LocalInstaller.
@ -115,6 +116,7 @@ func (i *LocalInstaller) installFromArchive() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to extract plugin metadata from tarball: %w", err) return fmt.Errorf("failed to extract plugin metadata from tarball: %w", err)
} }
i.pluginName = metadata.Name
filename := fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version) filename := fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
tarballPath := helmpath.DataPath("plugins", filename) tarballPath := helmpath.DataPath("plugins", filename)
if err := os.MkdirAll(filepath.Dir(tarballPath), 0o755); err != nil { if err := os.MkdirAll(filepath.Dir(tarballPath), 0o755); err != nil {
@ -147,10 +149,16 @@ func (i *LocalInstaller) installFromArchive() error {
} }
// Plugin directory should be named after the plugin at the archive root // Plugin directory should be named after the plugin at the archive root
pluginName := stripPluginName(filepath.Base(i.Source)) pluginDir := filepath.Join(tempDir, metadata.Name)
pluginDir := filepath.Join(tempDir, pluginName)
if _, err = os.Stat(filepath.Join(pluginDir, "plugin.yaml")); err != nil { if _, err = os.Stat(filepath.Join(pluginDir, "plugin.yaml")); err != nil {
return fmt.Errorf("plugin.yaml not found in expected directory %s: %w", pluginDir, err) // Stay backwards compatible with archives like foo.tgz containing foo/plugin.tgz regardless of actual plugin name
archiveBasedName := stripPluginName(filepath.Base(i.Source))
fallbackDir := filepath.Join(tempDir, archiveBasedName)
if _, err = os.Stat(filepath.Join(fallbackDir, "plugin.yaml")); err != nil {
return fmt.Errorf("plugin.yaml not found in expected directory %s: %w", pluginDir, err)
}
slog.Warn("plugin is using bad archive path", slog.String("path", archiveBasedName))
pluginDir = fallbackDir
} }
// Copy to the final destination // Copy to the final destination
@ -170,13 +178,19 @@ func (i *LocalInstaller) Path() string {
return "" return ""
} }
pluginName := filepath.Base(i.Source) return helmpath.DataPath("plugins", i.name())
if i.isArchive { }
// Strip archive extension to get plugin name
pluginName = stripPluginName(pluginName)
}
return helmpath.DataPath("plugins", pluginName) func (i *LocalInstaller) name() string {
if i.pluginName == "" {
pluginName := filepath.Base(i.Source)
if i.isArchive {
// Strip archive extension to get plugin name
pluginName = stripPluginName(pluginName)
}
i.pluginName = pluginName
}
return i.pluginName
} }
// SupportsVerification returns true if the local installer can verify plugins // SupportsVerification returns true if the local installer can verify plugins

@ -55,57 +55,103 @@ func TestLocalInstallerNotAFolder(t *testing.T) {
} }
func TestLocalInstallerTarball(t *testing.T) { func TestLocalInstallerTarball(t *testing.T) {
ensure.HelmHome(t) testCases := []struct {
Name string
// Create a test tarball Archive string
tempDir := t.TempDir() Files []struct {
tarballPath := filepath.Join(tempDir, "test-plugin-1.0.0.tar.gz") Name string
Body string
// Create tarball content Mode int64
var buf bytes.Buffer }
gw := gzip.NewWriter(&buf) ExpectedPath string
tw := tar.NewWriter(gw)
files := []struct {
Name string
Body string
Mode int64
}{ }{
{"test-plugin/plugin.yaml", "name: test-plugin\napiVersion: v1\ntype: cli/v1\nruntime: subprocess\nversion: 1.0.0\nconfig:\n shortHelp: test\n longHelp: test\nruntimeConfig:\n platformCommand:\n - command: echo", 0o644}, {
{"test-plugin/bin/test-plugin", "#!/usr/bin/env sh\necho test", 0o755}, Name: "plugin archive named following the convention",
Archive: "test-plugin-1.0.0.tar.gz",
Files: []struct {
Name string
Body string
Mode int64
}{
{"test-plugin/plugin.yaml", "name: test-plugin\napiVersion: v1\ntype: cli/v1\nruntime: subprocess\nversion: 1.0.0\nconfig:\n shortHelp: test\n longHelp: test\nruntimeConfig:\n platformCommand:\n - command: echo", 0o644},
{"test-plugin/bin/test-plugin", "#!/usr/bin/env sh\necho test", 0o755},
},
ExpectedPath: "test-plugin",
},
{
Name: "plugin archive with unconventional name",
Archive: "unconventional-name-linux-amd64.tar.gz",
Files: []struct {
Name string
Body string
Mode int64
}{
{"unconventional-name/plugin.yaml", "name: unconventional-name\napiVersion: v1\ntype: cli/v1\nruntime: subprocess\nversion: 1.0.0\nconfig:\n shortHelp: test\n longHelp: test\nruntimeConfig:\n platformCommand:\n - command: echo", 0o644},
{"unconventional-name/bin/test-plugin", "#!/usr/bin/env sh\necho test", 0o755},
},
ExpectedPath: "unconventional-name",
},
{
Name: "plugin where archive name matches sub-path",
Archive: "search-here-1.0.0.tar.gz",
Files: []struct {
Name string
Body string
Mode int64
}{
{"search-here/plugin.yaml", "name: working-plugin\napiVersion: v1\ntype: cli/v1\nruntime: subprocess\nversion: 1.0.0\nconfig:\n shortHelp: test\n longHelp: test\nruntimeConfig:\n platformCommand:\n - command: echo", 0o644},
{"search-here/bin/test-plugin", "#!/usr/bin/env sh\necho test", 0o755},
},
ExpectedPath: "working-plugin",
},
} }
for _, file := range files { for _, tt := range testCases {
hdr := &tar.Header{ t.Run(tt.Name, func(t *testing.T) {
Name: file.Name, ensure.HelmHome(t)
Mode: file.Mode,
Size: int64(len(file.Body)), // Create a test tarball
} tempDir := t.TempDir()
require.NoError(t, tw.WriteHeader(hdr)) tarballPath := filepath.Join(tempDir, tt.Archive)
_, err := tw.Write([]byte(file.Body))
require.NoError(t, err) // Create tarball content
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gw)
for _, file := range tt.Files {
hdr := &tar.Header{
Name: file.Name,
Mode: file.Mode,
Size: int64(len(file.Body)),
}
require.NoError(t, tw.WriteHeader(hdr))
_, err := tw.Write([]byte(file.Body))
require.NoError(t, err)
}
require.NoError(t, tw.Close())
require.NoError(t, gw.Close())
// Write tarball to file
require.NoError(t, os.WriteFile(tarballPath, buf.Bytes(), 0o644))
// Test installation
i, err := NewForSource(tarballPath, "")
require.NoError(t, err)
// Verify it's detected as LocalInstaller
localInstaller, ok := i.(*LocalInstaller)
require.True(t, ok, "expected LocalInstaller")
require.True(t, localInstaller.isArchive, "expected isArchive to be true")
require.NoError(t, Install(i))
expectedPath := helmpath.DataPath("plugins", tt.ExpectedPath)
require.Equal(t, expectedPath, i.Path(), "expected path %q, got %q", expectedPath, i.Path())
// Verify plugin was installed
_, err = os.Stat(i.Path())
require.NoErrorf(t, err, "plugin not found at %s", i.Path())
})
} }
require.NoError(t, tw.Close())
require.NoError(t, gw.Close())
// Write tarball to file
require.NoError(t, os.WriteFile(tarballPath, buf.Bytes(), 0o644))
// Test installation
i, err := NewForSource(tarballPath, "")
require.NoError(t, err)
// Verify it's detected as LocalInstaller
localInstaller, ok := i.(*LocalInstaller)
require.True(t, ok, "expected LocalInstaller")
require.True(t, localInstaller.isArchive, "expected isArchive to be true")
require.NoError(t, Install(i))
expectedPath := helmpath.DataPath("plugins", "test-plugin")
require.Equal(t, expectedPath, i.Path(), "expected path %q, got %q", expectedPath, i.Path())
// Verify plugin was installed
_, err = os.Stat(i.Path())
require.NoErrorf(t, err, "plugin not found at %s", i.Path())
} }

Loading…
Cancel
Save