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/artifact_installer.go

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
}