mirror of https://github.com/helm/helm
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.
205 lines
6.0 KiB
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
|
|
}
|