You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
helm/internal/plugin/installer/local_installer.go

205 lines
6.0 KiB

/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/v4/internal/plugin/installer"
import (
"bytes"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"helm.sh/helm/v4/internal/plugin"
"helm.sh/helm/v4/internal/third_party/dep/fs"
"helm.sh/helm/v4/pkg/helmpath"
)
// ErrPluginNotAFolder indicates that the plugin path is not a folder.
var ErrPluginNotAFolder = errors.New("expected plugin to be a folder")
// LocalInstaller installs plugins from the filesystem.
type LocalInstaller struct {
base
isArchive bool
extractor Extractor
provData []byte // Provenance data to save after installation
}
// NewLocalInstaller creates a new LocalInstaller.
func NewLocalInstaller(source string) (*LocalInstaller, error) {
src, err := filepath.Abs(source)
if err != nil {
return nil, fmt.Errorf("unable to get absolute path to plugin: %w", err)
}
i := &LocalInstaller{
base: newBase(src),
}
// Check if source is an archive
if isLocalArchive(src) {
i.isArchive = true
extractor, err := NewExtractor(src)
if err != nil {
return nil, fmt.Errorf("unsupported archive format: %w", err)
}
i.extractor = extractor
}
return i, nil
}
// isLocalArchive checks if the file is a supported archive format
func isLocalArchive(path string) bool {
for suffix := range Extractors {
if strings.HasSuffix(path, suffix) {
return true
}
}
return false
}
// Install creates a symlink to the plugin directory.
//
// Implements Installer.
func (i *LocalInstaller) Install() error {
if i.isArchive {
return i.installFromArchive()
}
return i.installFromDirectory()
}
// installFromDirectory creates a symlink to the plugin directory
func (i *LocalInstaller) installFromDirectory() error {
stat, err := os.Stat(i.Source)
if err != nil {
return err
}
if !stat.IsDir() {
return ErrPluginNotAFolder
}
if !isPlugin(i.Source) {
return ErrMissingMetadata
}
slog.Debug("symlinking", "source", i.Source, "path", i.Path())
return os.Symlink(i.Source, i.Path())
}
// installFromArchive extracts and installs a plugin from a tarball
func (i *LocalInstaller) installFromArchive() error {
// Read the archive file
data, err := os.ReadFile(i.Source)
if err != nil {
return fmt.Errorf("failed to read archive: %w", err)
}
// Copy the original tarball to plugins directory for verification
// Extract metadata to get the actual plugin name and version
metadata, err := plugin.ExtractPluginMetadataFromReader(bytes.NewReader(data))
if err != nil {
return fmt.Errorf("failed to extract plugin metadata from tarball: %w", err)
}
filename := fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
tarballPath := helmpath.DataPath("plugins", filename)
if err := os.MkdirAll(filepath.Dir(tarballPath), 0755); err != nil {
return fmt.Errorf("failed to create plugins directory: %w", err)
}
if err := os.WriteFile(tarballPath, data, 0644); err != nil {
return fmt.Errorf("failed to save tarball: %w", err)
}
// Check for and copy .prov file if it exists
provSource := i.Source + ".prov"
if provData, err := os.ReadFile(provSource); err == nil {
provPath := tarballPath + ".prov"
if err := os.WriteFile(provPath, provData, 0644); err != nil {
slog.Debug("failed to save provenance file", "error", err)
}
}
// Create a temporary directory for extraction
tempDir, err := os.MkdirTemp("", "helm-plugin-extract-")
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tempDir)
// Extract the archive
buffer := bytes.NewBuffer(data)
if err := i.extractor.Extract(buffer, tempDir); err != nil {
return fmt.Errorf("failed to extract archive: %w", err)
}
// Plugin directory should be named after the plugin at the archive root
pluginName := stripPluginName(filepath.Base(i.Source))
pluginDir := filepath.Join(tempDir, pluginName)
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)
}
// Copy to the final destination
slog.Debug("copying", "source", pluginDir, "path", i.Path())
return fs.CopyDir(pluginDir, i.Path())
}
// Update updates a local repository
func (i *LocalInstaller) Update() error {
slog.Debug("local repository is auto-updated")
return nil
}
// Path is overridden to handle archive plugin names properly
func (i *LocalInstaller) Path() string {
if i.Source == "" {
return ""
}
pluginName := filepath.Base(i.Source)
if i.isArchive {
// Strip archive extension to get plugin name
pluginName = stripPluginName(pluginName)
}
return helmpath.DataPath("plugins", pluginName)
}
// SupportsVerification returns true if the local installer can verify plugins
func (i *LocalInstaller) SupportsVerification() bool {
// Only support verification for local tarball files
return i.isArchive
}
// PrepareForVerification returns the local path for verification
func (i *LocalInstaller) PrepareForVerification() (string, func(), error) {
if !i.SupportsVerification() {
return "", nil, fmt.Errorf("verification not supported for directories")
}
// For local files, try to read the .prov file if it exists
provFile := i.Source + ".prov"
if provData, err := os.ReadFile(provFile); err == nil {
// Store the provenance data so we can save it after installation
i.provData = provData
}
// Note: We don't fail if .prov file doesn't exist - the verification logic
// in InstallWithOptions will handle missing .prov files appropriately
// Return the source path directly, no cleanup needed
return i.Source, nil, nil
}