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.
302 lines
8.3 KiB
302 lines
8.3 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 (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"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/getter"
|
|
"helm.sh/helm/v4/pkg/helmpath"
|
|
"helm.sh/helm/v4/pkg/registry"
|
|
)
|
|
|
|
// Ensure OCIInstaller implements Verifier
|
|
var _ Verifier = (*OCIInstaller)(nil)
|
|
|
|
// OCIInstaller installs plugins from OCI registries
|
|
type OCIInstaller struct {
|
|
CacheDir string
|
|
PluginName string
|
|
base
|
|
settings *cli.EnvSettings
|
|
getter getter.Getter
|
|
// Cached data to avoid duplicate downloads
|
|
pluginData []byte
|
|
provData []byte
|
|
}
|
|
|
|
// NewOCIInstaller creates a new OCIInstaller with optional getter options
|
|
func NewOCIInstaller(source string, options ...getter.Option) (*OCIInstaller, error) {
|
|
// Extract plugin name from OCI reference using robust registry parsing
|
|
pluginName, err := registry.GetPluginName(source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key, err := cache.Key(source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
settings := cli.New()
|
|
|
|
// Always add plugin artifact type and any provided options
|
|
pluginOptions := append([]getter.Option{getter.WithArtifactType("plugin")}, options...)
|
|
getterProvider, err := getter.NewOCIGetter(pluginOptions...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
i := &OCIInstaller{
|
|
CacheDir: helmpath.CachePath("plugins", key),
|
|
PluginName: pluginName,
|
|
base: newBase(source),
|
|
settings: settings,
|
|
getter: getterProvider,
|
|
}
|
|
return i, nil
|
|
}
|
|
|
|
// Install downloads and installs a plugin from OCI registry
|
|
// Implements Installer.
|
|
func (i *OCIInstaller) Install() error {
|
|
slog.Debug("pulling OCI plugin", "source", i.Source)
|
|
|
|
// Ensure plugin data is cached
|
|
if i.pluginData == nil {
|
|
pluginData, err := i.getter.Get(i.Source)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to pull plugin from %s: %w", i.Source, err)
|
|
}
|
|
i.pluginData = pluginData.Bytes()
|
|
}
|
|
|
|
// 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)
|
|
|
|
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)
|
|
}
|
|
|
|
// Ensure prov data is cached if available
|
|
if i.provData == nil {
|
|
// Try to download .prov file if it exists
|
|
provSource := i.Source + ".prov"
|
|
if provData, err := i.getter.Get(provSource); err == nil {
|
|
i.provData = provData.Bytes()
|
|
}
|
|
}
|
|
|
|
// 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.Debug("failed to save provenance file", "error", err)
|
|
}
|
|
}
|
|
|
|
// Check if this is a gzip compressed file
|
|
if len(i.pluginData) < 2 || i.pluginData[0] != 0x1f || i.pluginData[1] != 0x8b {
|
|
return fmt.Errorf("plugin data is not a gzip compressed archive")
|
|
}
|
|
|
|
// Create cache directory
|
|
if err := os.MkdirAll(i.CacheDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
|
}
|
|
|
|
// Extract as gzipped tar
|
|
if err := extractTarGz(bytes.NewReader(i.pluginData), i.CacheDir); err != nil {
|
|
return fmt.Errorf("failed to extract plugin: %w", err)
|
|
}
|
|
|
|
// Verify plugin.yaml exists - check root and subdirectories
|
|
pluginDir := i.CacheDir
|
|
if !isPlugin(pluginDir) {
|
|
// Check if plugin.yaml is in a subdirectory
|
|
entries, err := os.ReadDir(i.CacheDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
foundPluginDir := ""
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
subDir := filepath.Join(i.CacheDir, entry.Name())
|
|
if isPlugin(subDir) {
|
|
foundPluginDir = subDir
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if foundPluginDir == "" {
|
|
return ErrMissingMetadata
|
|
}
|
|
|
|
// Use the subdirectory as the plugin directory
|
|
pluginDir = foundPluginDir
|
|
}
|
|
|
|
// Copy from cache to final destination
|
|
src, err := filepath.Abs(pluginDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
slog.Debug("copying", "source", src, "path", i.Path())
|
|
return fs.CopyDir(src, i.Path())
|
|
}
|
|
|
|
// Update updates a plugin by reinstalling it
|
|
func (i *OCIInstaller) Update() error {
|
|
// For OCI, update means removing the old version and installing the new one
|
|
if err := os.RemoveAll(i.Path()); err != nil {
|
|
return err
|
|
}
|
|
return i.Install()
|
|
}
|
|
|
|
// Path is where the plugin will be installed
|
|
func (i OCIInstaller) Path() string {
|
|
if i.Source == "" {
|
|
return ""
|
|
}
|
|
return filepath.Join(i.settings.PluginsDirectory, i.PluginName)
|
|
}
|
|
|
|
// extractTarGz extracts a gzipped tar archive to a directory
|
|
func extractTarGz(r io.Reader, targetDir string) error {
|
|
gzr, err := gzip.NewReader(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer gzr.Close()
|
|
|
|
return extractTar(gzr, targetDir)
|
|
}
|
|
|
|
// extractTar extracts a tar archive to a directory
|
|
func extractTar(r io.Reader, targetDir string) error {
|
|
tarReader := tar.NewReader(r)
|
|
|
|
for {
|
|
header, err := tarReader.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path, err := cleanJoin(targetDir, header.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch header.Typeflag {
|
|
case tar.TypeDir:
|
|
if err := os.MkdirAll(path, 0755); err != nil {
|
|
return err
|
|
}
|
|
case tar.TypeReg:
|
|
dir := filepath.Dir(path)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
outFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer outFile.Close()
|
|
if _, err := io.Copy(outFile, tarReader); err != nil {
|
|
return err
|
|
}
|
|
case tar.TypeXGlobalHeader, tar.TypeXHeader:
|
|
// Skip these
|
|
continue
|
|
default:
|
|
return fmt.Errorf("unknown type: %b in %s", header.Typeflag, header.Name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SupportsVerification returns true since OCI plugins can be verified
|
|
func (i *OCIInstaller) SupportsVerification() bool {
|
|
return true
|
|
}
|
|
|
|
// GetVerificationData downloads and caches plugin and provenance data from OCI registry for verification
|
|
func (i *OCIInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) {
|
|
slog.Debug("getting verification data for OCI plugin", "source", i.Source)
|
|
|
|
// Download plugin data once and cache it
|
|
if i.pluginData == nil {
|
|
pluginDataBuffer, err := i.getter.Get(i.Source)
|
|
if err != nil {
|
|
return nil, nil, "", fmt.Errorf("failed to pull plugin from %s: %w", i.Source, err)
|
|
}
|
|
i.pluginData = pluginDataBuffer.Bytes()
|
|
}
|
|
|
|
// Download prov data once and cache it if available
|
|
if i.provData == nil {
|
|
provSource := i.Source + ".prov"
|
|
// Calling getter.Get again is reasonable because: 1. The OCI registry client already optimizes the underlying network calls
|
|
// 2. Both calls use the same underlying manifest and memory store 3. The second .prov call is very fast since the data is already pulled
|
|
provDataBuffer, err := i.getter.Get(provSource)
|
|
if err != nil {
|
|
// If provenance file doesn't exist, set provData to nil
|
|
// The verification logic will handle this gracefully
|
|
i.provData = nil
|
|
} else {
|
|
i.provData = provDataBuffer.Bytes()
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
slog.Debug("got verification data for OCI plugin", "filename", filename)
|
|
return i.pluginData, i.provData, filename, nil
|
|
}
|