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.
219 lines
6.4 KiB
219 lines
6.4 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 (
|
|
"bytes"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"helm.sh/helm/v4/internal/plugin"
|
|
"helm.sh/helm/v4/internal/plugin/cache"
|
|
"helm.sh/helm/v4/internal/third_party/dep/fs"
|
|
"helm.sh/helm/v4/pkg/cli"
|
|
"helm.sh/helm/v4/pkg/downloader"
|
|
"helm.sh/helm/v4/pkg/getter"
|
|
"helm.sh/helm/v4/pkg/helmpath"
|
|
)
|
|
|
|
// ArtifactInstaller installs plugins using the unified artifact downloader.
|
|
type ArtifactInstaller struct {
|
|
CacheDir string
|
|
PluginName string
|
|
base
|
|
downloader *downloader.Downloader
|
|
settings *cli.EnvSettings
|
|
version string // Version constraint for VCS sources
|
|
// Cached data to avoid duplicate downloads
|
|
pluginData []byte
|
|
provData []byte
|
|
}
|
|
|
|
// NewArtifactInstaller creates a new ArtifactInstaller.
|
|
func NewArtifactInstaller(source string) (*ArtifactInstaller, error) {
|
|
key, err := cache.Key(source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
settings := cli.New()
|
|
downloader := &downloader.Downloader{}
|
|
|
|
i := &ArtifactInstaller{
|
|
CacheDir: helmpath.CachePath("plugins", key),
|
|
PluginName: stripPluginName(filepath.Base(source)),
|
|
base: newBase(source),
|
|
downloader: downloader,
|
|
settings: settings,
|
|
}
|
|
return i, nil
|
|
}
|
|
|
|
// Install downloads and installs the plugin.
|
|
func (i *ArtifactInstaller) Install() error {
|
|
// Download plugin using unified downloader
|
|
if i.pluginData == nil {
|
|
tempDir := i.CacheDir + "-download"
|
|
if err := os.MkdirAll(tempDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create temp download directory: %w", err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
pluginPath, _, err := i.downloader.Download(i.Source, i.version, tempDir, downloader.TypePlugin)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to download plugin: %w", err)
|
|
}
|
|
|
|
// Read the downloaded data
|
|
data, err := os.ReadFile(pluginPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read downloaded plugin: %w", err)
|
|
}
|
|
i.pluginData = data
|
|
|
|
// Try to get provenance data
|
|
provPath := pluginPath + ".prov"
|
|
if provData, err := os.ReadFile(provPath); err == nil {
|
|
i.provData = provData
|
|
}
|
|
}
|
|
|
|
// Extract metadata to get the actual plugin name and version
|
|
metadata, err := plugin.ExtractTgzPluginMetadata(bytes.NewReader(i.pluginData))
|
|
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)
|
|
|
|
// Save the original tarball to plugins directory
|
|
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, i.pluginData, 0644); err != nil {
|
|
return fmt.Errorf("failed to save tarball: %w", err)
|
|
}
|
|
|
|
// Save prov file if we have the data
|
|
if i.provData != nil {
|
|
provPath := tarballPath + ".prov"
|
|
if err := os.WriteFile(provPath, i.provData, 0644); err != nil {
|
|
slog.Warn("failed to write provenance file", "path", provPath, "error", err)
|
|
}
|
|
}
|
|
|
|
// Extract plugin to cache directory
|
|
extractor, err := NewExtractor(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := extractor.Extract(bytes.NewBuffer(i.pluginData), i.CacheDir); err != nil {
|
|
return fmt.Errorf("extracting files from archive: %w", err)
|
|
}
|
|
|
|
// Detect where the plugin.yaml actually is
|
|
pluginRoot, err := detectPluginRoot(i.CacheDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate plugin structure if needed
|
|
if err := validatePluginName(pluginRoot, i.PluginName); err != nil {
|
|
return err
|
|
}
|
|
|
|
src, err := filepath.Abs(pluginRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return fs.CopyDir(src, i.Path())
|
|
}
|
|
|
|
// Update updates a plugin by reinstalling it.
|
|
func (i *ArtifactInstaller) Update() error {
|
|
if err := os.RemoveAll(i.Path()); err != nil {
|
|
return err
|
|
}
|
|
return i.Install()
|
|
}
|
|
|
|
// Path returns where the plugin will be installed.
|
|
func (i *ArtifactInstaller) Path() string {
|
|
if i.Source == "" {
|
|
return ""
|
|
}
|
|
return filepath.Join(i.settings.PluginsDirectory, i.PluginName)
|
|
}
|
|
|
|
// SupportsVerification returns true if the installer can verify plugins.
|
|
func (i *ArtifactInstaller) SupportsVerification() bool {
|
|
return true // The unified downloader supports verification
|
|
}
|
|
|
|
// SetOptions sets additional options for the downloader.
|
|
func (i *ArtifactInstaller) SetOptions(options []getter.Option) {
|
|
i.downloader.Options = append(i.downloader.Options, options...)
|
|
}
|
|
|
|
// SetVersion sets the version constraint for the plugin download.
|
|
func (i *ArtifactInstaller) SetVersion(version string) {
|
|
i.version = version
|
|
}
|
|
|
|
// GetVerificationData returns cached plugin and provenance data for verification.
|
|
func (i *ArtifactInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) {
|
|
// Ensure data is cached
|
|
if i.pluginData == nil {
|
|
// Download plugin using unified downloader
|
|
tempDir := i.CacheDir + "-verification"
|
|
if err := os.MkdirAll(tempDir, 0755); err != nil {
|
|
return nil, nil, "", fmt.Errorf("failed to create temp verification directory: %w", err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
pluginPath, _, err := i.downloader.Download(i.Source, i.version, tempDir, downloader.TypePlugin)
|
|
if err != nil {
|
|
return nil, nil, "", fmt.Errorf("failed to download plugin for verification: %w", err)
|
|
}
|
|
|
|
// Read the downloaded data
|
|
data, err := os.ReadFile(pluginPath)
|
|
if err != nil {
|
|
return nil, nil, "", fmt.Errorf("failed to read downloaded plugin: %w", err)
|
|
}
|
|
i.pluginData = data
|
|
|
|
// Try to get provenance data
|
|
provPath := pluginPath + ".prov"
|
|
if provData, err := os.ReadFile(provPath); err == nil {
|
|
i.provData = provData
|
|
}
|
|
}
|
|
|
|
// Extract metadata to get the filename
|
|
metadata, err := plugin.ExtractTgzPluginMetadata(bytes.NewReader(i.pluginData))
|
|
if err != nil {
|
|
return nil, nil, "", fmt.Errorf("failed to extract plugin metadata from tarball: %w", err)
|
|
}
|
|
filename = fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
|
|
|
|
return i.pluginData, i.provData, filename, nil
|
|
}
|