mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 05:19:39 +02:00
feat(oci): oci helm support [r8s-361] (#787)
This commit is contained in:
parent
b6a6ce9aaf
commit
2697d6c5d7
80 changed files with 4264 additions and 812 deletions
|
@ -1,24 +1,38 @@
|
|||
package sdk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/portainer/pkg/libhelm/options"
|
||||
"github.com/portainer/portainer/pkg/libhelm/release"
|
||||
"github.com/rs/zerolog/log"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
"helm.sh/helm/v3/pkg/downloader"
|
||||
"helm.sh/helm/v3/pkg/getter"
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
)
|
||||
|
||||
// Helm chart reference label constants
|
||||
const (
|
||||
ChartPathAnnotation = "portainer/chart-path"
|
||||
RepoURLAnnotation = "portainer/repo-url"
|
||||
RegistryIDAnnotation = "portainer/registry-id"
|
||||
)
|
||||
|
||||
// loadAndValidateChartWithPathOptions locates and loads the chart, and validates it.
|
||||
// it also checks for chart dependencies and updates them if necessary.
|
||||
// it returns the chart information.
|
||||
func (hspm *HelmSDKPackageManager) loadAndValidateChartWithPathOptions(chartPathOptions *action.ChartPathOptions, chartName, version string, repoURL string, dependencyUpdate bool, operation string) (*chart.Chart, error) {
|
||||
// Locate and load the chart
|
||||
chartPathOptions.RepoURL = repoURL
|
||||
chartPathOptions.Version = version
|
||||
chartPath, err := chartPathOptions.LocateChart(chartName, hspm.settings)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
|
@ -26,6 +40,11 @@ func (hspm *HelmSDKPackageManager) loadAndValidateChartWithPathOptions(chartPath
|
|||
Str("chart", chartName).
|
||||
Err(err).
|
||||
Msg("Failed to locate chart for helm " + operation)
|
||||
|
||||
// For OCI charts, chartName already contains the full reference
|
||||
if strings.HasPrefix(chartName, options.OCIProtocolPrefix) {
|
||||
return nil, errors.Wrapf(err, "failed to find the helm chart: %s", chartName)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "failed to find the helm chart at the path: %s/%s", repoURL, chartName)
|
||||
}
|
||||
|
||||
|
@ -86,3 +105,186 @@ func (hspm *HelmSDKPackageManager) loadAndValidateChartWithPathOptions(chartPath
|
|||
|
||||
return chartReq, nil
|
||||
}
|
||||
|
||||
// parseRepoURL parses and validates a Helm repository URL using RFC 3986 standards.
|
||||
// Used by search and show operations before downloading index.yaml files.
|
||||
func parseRepoURL(repoURL string) (*url.URL, error) {
|
||||
parsedURL, err := url.ParseRequestURI(repoURL)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid helm chart URL: "+repoURL)
|
||||
}
|
||||
return parsedURL, nil
|
||||
}
|
||||
|
||||
// getRepoNameFromURL generates a unique repository identifier from a URL.
|
||||
// Combines hostname and path for uniqueness (e.g., "charts.helm.sh/stable" → "charts.helm.sh-stable").
|
||||
// Used for Helm's repositories.yaml entries, caching, and chart references.
|
||||
func getRepoNameFromURL(urlStr string) (string, error) {
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse URL: %w", err)
|
||||
}
|
||||
|
||||
hostname := parsedURL.Hostname()
|
||||
path := parsedURL.Path
|
||||
path = strings.Trim(path, "/")
|
||||
path = strings.ReplaceAll(path, "/", "-")
|
||||
|
||||
if path == "" {
|
||||
return hostname, nil
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", hostname, path), nil
|
||||
}
|
||||
|
||||
// loadIndexFile loads and parses a Helm repository index.yaml file.
|
||||
// Called after downloading from HTTP repos or generating from OCI registries.
|
||||
// Contains chart metadata used for discovery, version resolution, and caching.
|
||||
func loadIndexFile(indexPath string) (*repo.IndexFile, error) {
|
||||
log.Debug().
|
||||
Str("context", "HelmClient").
|
||||
Str("index_path", indexPath).
|
||||
Msg("Loading index file")
|
||||
|
||||
indexFile, err := repo.LoadIndexFile(indexPath)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("context", "HelmClient").
|
||||
Str("index_path", indexPath).
|
||||
Err(err).
|
||||
Msg("Failed to load index file")
|
||||
return nil, errors.Wrapf(err, "failed to load downloaded index file: %s", indexPath)
|
||||
}
|
||||
return indexFile, nil
|
||||
}
|
||||
|
||||
// ensureHelmDirectoriesExist creates required Helm directories and configuration files.
|
||||
// Creates repository cache, config directories, and ensures repositories.yaml exists.
|
||||
// Essential for Helm operations to function properly.
|
||||
func ensureHelmDirectoriesExist(settings *cli.EnvSettings) error {
|
||||
log.Debug().
|
||||
Str("context", "helm_sdk_dirs").
|
||||
Msg("Ensuring Helm directories exist")
|
||||
|
||||
// List of directories to ensure exist
|
||||
directories := []string{
|
||||
filepath.Dir(settings.RepositoryConfig), // Repository config directory
|
||||
settings.RepositoryCache, // Repository cache directory
|
||||
filepath.Dir(settings.RegistryConfig), // Registry config directory
|
||||
settings.PluginsDirectory, // Plugins directory
|
||||
}
|
||||
|
||||
// Create each directory if it doesn't exist
|
||||
for _, dir := range directories {
|
||||
if dir == "" {
|
||||
continue // Skip empty paths
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
log.Error().
|
||||
Str("context", "helm_sdk_dirs").
|
||||
Str("directory", dir).
|
||||
Err(err).
|
||||
Msg("Failed to create directory")
|
||||
return errors.Wrapf(err, "failed to create directory: %s", dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure registry config file exists
|
||||
if settings.RegistryConfig != "" {
|
||||
if _, err := os.Stat(settings.RegistryConfig); os.IsNotExist(err) {
|
||||
// Create the directory if it doesn't exist
|
||||
dir := filepath.Dir(settings.RegistryConfig)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
log.Error().
|
||||
Str("context", "helm_sdk_dirs").
|
||||
Str("directory", dir).
|
||||
Err(err).
|
||||
Msg("Failed to create directory")
|
||||
return errors.Wrapf(err, "failed to create directory: %s", dir)
|
||||
}
|
||||
|
||||
// Create an empty registry config file
|
||||
if _, err := os.Create(settings.RegistryConfig); err != nil {
|
||||
log.Error().
|
||||
Str("context", "helm_sdk_dirs").
|
||||
Str("file", settings.RegistryConfig).
|
||||
Err(err).
|
||||
Msg("Failed to create registry config file")
|
||||
return errors.Wrapf(err, "failed to create registry config file: %s", settings.RegistryConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure repository config file exists
|
||||
if settings.RepositoryConfig != "" {
|
||||
if _, err := os.Stat(settings.RepositoryConfig); os.IsNotExist(err) {
|
||||
// Create an empty repository config file with default yaml structure
|
||||
f := repo.NewFile()
|
||||
if err := f.WriteFile(settings.RepositoryConfig, 0644); err != nil {
|
||||
log.Error().
|
||||
Str("context", "helm_sdk_dirs").
|
||||
Str("file", settings.RepositoryConfig).
|
||||
Err(err).
|
||||
Msg("Failed to create repository config file")
|
||||
return errors.Wrapf(err, "failed to create repository config file: %s", settings.RepositoryConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("context", "helm_sdk_dirs").
|
||||
Msg("Successfully ensured all Helm directories exist")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// appendChartReferenceAnnotations encodes chart reference values for safe storage in Helm labels.
|
||||
// It creates a new map with encoded values for specific chart reference labels.
|
||||
// Preserves existing labels and handles edge cases gracefully.
|
||||
func appendChartReferenceAnnotations(chartPath, repoURL string, registryID int, existingAnnotations map[string]string) map[string]string {
|
||||
// Copy existing annotations
|
||||
annotations := make(map[string]string)
|
||||
maps.Copy(annotations, existingAnnotations)
|
||||
|
||||
// delete the existing portainer specific labels, for a clean overwrite
|
||||
delete(annotations, ChartPathAnnotation)
|
||||
delete(annotations, RepoURLAnnotation)
|
||||
delete(annotations, RegistryIDAnnotation)
|
||||
|
||||
if chartPath != "" {
|
||||
annotations[ChartPathAnnotation] = chartPath
|
||||
}
|
||||
|
||||
if repoURL != "" && registryID == 0 {
|
||||
annotations[RepoURLAnnotation] = repoURL
|
||||
}
|
||||
|
||||
if registryID != 0 {
|
||||
annotations[RegistryIDAnnotation] = strconv.Itoa(registryID)
|
||||
}
|
||||
|
||||
return annotations
|
||||
}
|
||||
|
||||
// extractChartReferenceAnnotations decodes chart reference labels for display purposes.
|
||||
// It handles existing labels gracefully and only decodes known chart reference labels.
|
||||
// If a chart reference label cannot be decoded, it is omitted entirely from the result.
|
||||
// Returns a ChartReference struct with decoded values.
|
||||
func extractChartReferenceAnnotations(annotations map[string]string) release.ChartReference {
|
||||
if annotations == nil {
|
||||
return release.ChartReference{}
|
||||
}
|
||||
|
||||
registryID, err := strconv.Atoi(annotations[RegistryIDAnnotation])
|
||||
if err != nil {
|
||||
registryID = 0
|
||||
}
|
||||
|
||||
return release.ChartReference{
|
||||
ChartPath: annotations[ChartPathAnnotation],
|
||||
RepoURL: annotations[RepoURLAnnotation],
|
||||
RegistryID: int64(registryID),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue