1
0
Fork 0
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:
Ali 2025-07-13 10:37:43 +12:00 committed by GitHub
parent b6a6ce9aaf
commit 2697d6c5d7
80 changed files with 4264 additions and 812 deletions

View file

@ -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),
}
}