Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions store/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import (
const (
helmOperationTimeout = 5 * time.Minute
helmInstallTimeout = 10 * time.Minute
helmCompatibilityTimeout = 2 * time.Minute
helmDiagnosticsTimeout = 15 * time.Second
helmDiagnosticsMaxEvents = 20
helmDiagnosticsMessageLen = 240
Expand Down Expand Up @@ -317,6 +318,9 @@ func FetchRepoIndex(repoURL string) ([]HelmChartSummary, error) {
continue
}
v := versions[0]
if !isInstallableHelmChartMetadata(v.Metadata) {
continue
}
charts = append(charts, HelmChartSummary{
Name: name,
Version: v.Version,
Expand Down Expand Up @@ -478,6 +482,9 @@ func fetchOCIChartSummary(repoURL string) ([]HelmChartSummary, error) {
if meta == nil {
return nil, fmt.Errorf("chart metadata not found for %s", redactURLForError(repoURL))
}
if !isInstallableHelmChartMetadata(meta) {
return []HelmChartSummary{}, nil
}
return []HelmChartSummary{
{
Name: meta.Name,
Expand All @@ -490,6 +497,10 @@ func fetchOCIChartSummary(repoURL string) ([]HelmChartSummary, error) {
}, nil
}

func isInstallableHelmChartMetadata(metadata *chart.Metadata) bool {
return metadata != nil && !strings.EqualFold(strings.TrimSpace(metadata.Type), "library")
}

// ---------- Chart loader ----------

func redactURLForError(raw string) string {
Expand Down Expand Up @@ -1208,17 +1219,24 @@ func InstallHelmChart(cfg *rest.Config, releaseName, namespace, chartName, repoU
}

attachHelmCapabilities(actionConfig, cfg, namespace, helmWarningLog)
if err := validateHelmChartCompatibility(context.Background(), cfg, actionConfig, releaseName, namespace, ch, vals); err != nil {
return err
}
install := action.NewInstall(actionConfig)
install.ReleaseName = releaseName
install.Namespace = namespace
install.CreateNamespace = true
install.Wait = true
install.WaitForJobs = true
install.Timeout = helmInstallTimeout

_, err = install.Run(ch, vals)
if err != nil {
return withHelmReleaseDiagnostics(context.Background(), cfg, releaseName, namespace, err)
}
reportHelmReadiness(inspectHelmReleaseResources(context.Background(), cfg, releaseName, namespace), func(message string) {
logrus.Warn(message)
})
return nil
}

Expand Down Expand Up @@ -1292,6 +1310,11 @@ func InstallHelmChartStream(ctx context.Context, lifecycle HelmInstallLifecycle,
return
}
attachHelmCapabilities(actionConfig, cfg, namespace, logFn)
if err := validateHelmChartCompatibility(installCtx, cfg, actionConfig, releaseName, namespace, helmChart, vals); err != nil {
send("ERROR: " + err.Error())
_ = lifecycle.Finish(err)
return
}
if err := lifecycle.MarkInstalling(); err != nil {
send("ERROR: " + err.Error())
if finishErr := lifecycle.Finish(err); finishErr != nil {
Expand All @@ -1304,6 +1327,7 @@ func InstallHelmChartStream(ctx context.Context, lifecycle HelmInstallLifecycle,
install.Namespace = namespace
install.CreateNamespace = true
install.Wait = true
install.WaitForJobs = true
install.Timeout = helmInstallTimeout
if _, err = install.RunWithContext(installCtx, helmChart, vals); err != nil {
for _, line := range helmReleaseDiagnostics(installCtx, cfg, releaseName, namespace) {
Expand All @@ -1315,6 +1339,9 @@ func InstallHelmChartStream(ctx context.Context, lifecycle HelmInstallLifecycle,
}
return
}
reportHelmReadiness(inspectHelmReleaseResources(installCtx, cfg, releaseName, namespace), func(message string) {
send("WARNING: " + message)
})
if err := lifecycle.Finish(nil); err != nil {
logrus.Warnf("failed to finish Helm operation: %v", err)
select {
Expand Down Expand Up @@ -1346,25 +1373,45 @@ func UpgradeHelmRelease(cfg *rest.Config, releaseName, namespace, chartName, rep
}

attachHelmCapabilities(actionConfig, cfg, namespace, helmWarningLog)
if err := validateHelmReleaseCompatibility(context.Background(), actionConfig, releaseName, namespace, ch, vals); err != nil {
return err
}
upgrade := action.NewUpgrade(actionConfig)
upgrade.Namespace = namespace
upgrade.Wait = true
upgrade.WaitForJobs = true
upgrade.Timeout = helmOperationTimeout

_, err = upgrade.Run(releaseName, ch, vals)
return err
if err != nil {
return withHelmReleaseDiagnostics(context.Background(), cfg, releaseName, namespace, err)
}
reportHelmReadiness(inspectHelmReleaseResources(context.Background(), cfg, releaseName, namespace), func(message string) {
logrus.Warn(message)
})
return nil
}

func RollbackHelmRelease(cfg *rest.Config, releaseName, namespace string, revision int) error {
actionConfig, err := newHelmConfig(cfg, namespace)
if err != nil {
return err
}
if err := validateHelmRollbackCompatibility(context.Background(), actionConfig, releaseName, namespace, revision); err != nil {
return err
}
rollback := action.NewRollback(actionConfig)
rollback.Version = revision
rollback.Wait = true
rollback.WaitForJobs = true
rollback.Timeout = helmOperationTimeout
return rollback.Run(releaseName)
if err := rollback.Run(releaseName); err != nil {
return withHelmReleaseDiagnostics(context.Background(), cfg, releaseName, namespace, err)
}
reportHelmReadiness(inspectHelmReleaseResources(context.Background(), cfg, releaseName, namespace), func(message string) {
logrus.Warn(message)
})
return nil
}

func UninstallHelmRelease(cfg *rest.Config, releaseName, namespace string) error {
Expand Down
118 changes: 118 additions & 0 deletions store/helm_compatibility.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package store

import (
"context"
"fmt"

"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

func validateHelmChartCompatibility(ctx context.Context, cfg *rest.Config, actionConfig *action.Configuration, releaseName, namespace string, chartToInstall *chart.Chart, values map[string]interface{}) error {
if chartToInstall == nil || chartToInstall.Metadata == nil {
return fmt.Errorf("chart metadata is missing")
}
if !isInstallableHelmChartMetadata(chartToInstall.Metadata) {
return fmt.Errorf("chart %s is a library chart and cannot be installed as an application", chartToInstall.Name())
}
if ctx == nil {
ctx = context.Background()
}
ctx, cancel := context.WithTimeout(ctx, helmCompatibilityTimeout)
defer cancel()
namespaceExists, err := helmNamespaceExists(ctx, cfg, namespace)
if err != nil {
return err
}
// Helm's dry-run validates rendering and resource structure against the
// target cluster. Remote template lookups are only safe after the release
// namespace exists; the real install remains the source of truth.
dryRun := newHelmCompatibilityDryRun(actionConfig, releaseName, namespace, namespaceExists)
_, err = dryRun.RunWithContext(ctx, chartToInstall, values)
if err != nil {
return fmt.Errorf("render chart %s for compatibility check: %w", chartToInstall.Name(), err)
}
return nil
}

func validateHelmReleaseCompatibility(ctx context.Context, actionConfig *action.Configuration, releaseName, namespace string, chartToInstall *chart.Chart, values map[string]interface{}) error {
if chartToInstall == nil || chartToInstall.Metadata == nil {
return fmt.Errorf("chart metadata is missing")
}
if !isInstallableHelmChartMetadata(chartToInstall.Metadata) {
return fmt.Errorf("chart %s is a library chart and cannot be installed as an application", chartToInstall.Name())
}
if ctx == nil {
ctx = context.Background()
}
ctx, cancel := context.WithTimeout(ctx, helmCompatibilityTimeout)
defer cancel()

dryRun := action.NewUpgrade(actionConfig)
dryRun.Namespace = namespace
dryRun.DryRun = true
dryRun.DryRunOption = "server"
dryRun.Wait = true
dryRun.WaitForJobs = true
dryRun.Timeout = helmCompatibilityTimeout
if _, err := dryRun.RunWithContext(ctx, releaseName, chartToInstall, values); err != nil {
return fmt.Errorf("render Helm release %s for compatibility check: %w", releaseName, err)
}
return nil
}

func validateHelmRollbackCompatibility(ctx context.Context, actionConfig *action.Configuration, releaseName, namespace string, revision int) error {
currentRelease, err := actionConfig.Releases.Last(releaseName)
if err != nil {
return fmt.Errorf("read current Helm release %s: %w", releaseName, err)
}
targetRevision := revision
if targetRevision == 0 {
targetRevision = currentRelease.Version - 1
}
if targetRevision <= 0 {
return fmt.Errorf("release %s has no prior revision to roll back to", releaseName)
}
target, err := actionConfig.Releases.Get(releaseName, targetRevision)
if err != nil {
return fmt.Errorf("read Helm release %s revision %d: %w", releaseName, targetRevision, err)
}
if err := validateHelmReleaseCompatibility(ctx, actionConfig, releaseName, namespace, target.Chart, target.Config); err != nil {
return err
}
return nil
}

func newHelmCompatibilityDryRun(actionConfig *action.Configuration, releaseName, namespace string, remoteLookup bool) *action.Install {
dryRun := action.NewInstall(actionConfig)
dryRun.ReleaseName = releaseName
dryRun.Namespace = namespace
dryRun.CreateNamespace = true
dryRun.DryRun = true
dryRun.DryRunOption = "client"
if remoteLookup {
dryRun.DryRunOption = "server"
}
dryRun.Timeout = helmCompatibilityTimeout
return dryRun
}

func helmNamespaceExists(ctx context.Context, cfg *rest.Config, namespace string) (bool, error) {
if cfg == nil {
return false, fmt.Errorf("Helm compatibility REST config is nil")
}
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
return false, fmt.Errorf("create Helm compatibility client: %w", err)
}
if _, err := client.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{}); apierrors.IsNotFound(err) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("check Helm release namespace %s: %w", namespace, err)
}
return true, nil
}
Loading
Loading