1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-18 21:09:40 +02:00

feat(helm): use helm upgrade for install [r8s-258] (#568)

This commit is contained in:
Ali 2025-03-26 11:32:26 +13:00 committed by GitHub
parent e68bd53e30
commit 0ebfe047d1
19 changed files with 613 additions and 150 deletions

View file

@ -42,7 +42,7 @@ func Test_helmDelete(t *testing.T) {
// Install a single chart directly, to be deleted by the handler
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}
h.helmPackageManager.Install(options)
h.helmPackageManager.Upgrade(options)
t.Run("helmDelete succeeds with admin user", func(t *testing.T) {
req := httptest.NewRequest(http.MethodDelete, "/1/kubernetes/helm/"+options.Name, nil)

View file

@ -125,7 +125,7 @@ func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*r
installOpts.ValuesFile = file.Name()
}
release, err := handler.helmPackageManager.Install(installOpts)
release, err := handler.helmPackageManager.Upgrade(installOpts)
if err != nil {
return nil, err
}

View file

@ -43,7 +43,7 @@ func Test_helmList(t *testing.T) {
// Install a single chart. We expect to get these values back
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}
h.helmPackageManager.Install(options)
h.helmPackageManager.Upgrade(options)
t.Run("helmList", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/1/kubernetes/helm", nil)

View file

@ -1,5 +1,7 @@
package options
import "time"
type InstallOptions struct {
Name string
Chart string
@ -8,6 +10,7 @@ type InstallOptions struct {
Wait bool
ValuesFile string
PostRenderer string
Timeout time.Duration
KubernetesClusterAccess *KubernetesClusterAccess
// Optional environment vars to pass when running helm

87
pkg/libhelm/sdk/common.go Normal file
View file

@ -0,0 +1,87 @@
package sdk
import (
"os"
"github.com/pkg/errors"
"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/downloader"
"helm.sh/helm/v3/pkg/getter"
)
// 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, repoURL string, dependencyUpdate bool, operation string) (*chart.Chart, error) {
// Locate and load the chart
chartPathOptions.RepoURL = repoURL
chartPath, err := chartPathOptions.LocateChart(chartName, hspm.settings)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart", chartName).
Err(err).
Msg("Failed to locate chart for helm " + operation)
return nil, errors.Wrapf(err, "failed to find the helm chart at the path: %s/%s", repoURL, chartName)
}
chartReq, err := loader.Load(chartPath)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart_path", chartPath).
Err(err).
Msg("Failed to load chart for helm " + operation)
return nil, errors.Wrap(err, "failed to load chart for helm "+operation)
}
// Check chart dependencies to make sure all are present in /charts
if chartDependencies := chartReq.Metadata.Dependencies; chartDependencies != nil {
if err := action.CheckDependencies(chartReq, chartDependencies); err != nil {
err = errors.Wrap(err, "failed to check chart dependencies for helm "+operation)
if !dependencyUpdate {
return nil, err
}
log.Debug().
Str("context", "HelmClient").
Str("chart", chartName).
Msg("Updating chart dependencies for helm " + operation)
providers := getter.All(hspm.settings)
manager := &downloader.Manager{
Out: os.Stdout,
ChartPath: chartPath,
Keyring: chartPathOptions.Keyring,
SkipUpdate: false,
Getters: providers,
RepositoryConfig: hspm.settings.RepositoryConfig,
RepositoryCache: hspm.settings.RepositoryCache,
Debug: hspm.settings.Debug,
}
if err := manager.Update(); err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart", chartName).
Err(err).
Msg("Failed to update chart dependencies for helm " + operation)
return nil, errors.Wrap(err, "failed to update chart dependencies for helm "+operation)
}
// Reload the chart with the updated Chart.lock file.
if chartReq, err = loader.Load(chartPath); err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart_path", chartPath).
Err(err).
Msg("Failed to reload chart after dependency update for helm " + operation)
return nil, errors.Wrap(err, "failed to reload chart after dependency update for helm "+operation)
}
}
}
return chartReq, nil
}

View file

@ -1,22 +1,18 @@
package sdk
import (
"os"
"time"
"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/downloader"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/postrender"
)
// Install implements the HelmPackageManager interface by using the Helm SDK to install a chart.
func (hspm *HelmSDKPackageManager) Install(installOpts options.InstallOptions) (*release.Release, error) {
func (hspm *HelmSDKPackageManager) install(installOpts options.InstallOptions) (*release.Release, error) {
log.Debug().
Str("context", "HelmClient").
Str("chart", installOpts.Chart).
@ -42,12 +38,7 @@ func (hspm *HelmSDKPackageManager) Install(installOpts options.InstallOptions) (
actionConfig := new(action.Configuration)
err := hspm.initActionConfig(actionConfig, installOpts.Namespace, installOpts.KubernetesClusterAccess)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart", installOpts.Chart).
Str("namespace", installOpts.Namespace).
Err(err).
Msg("Failed to initialize helm configuration for helm release installation")
// error is already logged in initActionConfig
return nil, errors.Wrap(err, "failed to initialize helm configuration for helm release installation")
}
@ -69,7 +60,7 @@ func (hspm *HelmSDKPackageManager) Install(installOpts options.InstallOptions) (
return nil, errors.Wrap(err, "failed to get Helm values from file for helm release installation")
}
chart, err := hspm.loadAndValidateChart(installClient, installOpts)
chart, err := hspm.loadAndValidateChartWithPathOptions(&installClient.ChartPathOptions, installOpts.Chart, installOpts.Repo, installClient.DependencyUpdate, "release installation")
if err != nil {
log.Error().
Str("context", "HelmClient").
@ -114,90 +105,29 @@ func (hspm *HelmSDKPackageManager) Install(installOpts options.InstallOptions) (
}, nil
}
// loadAndValidateChart 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) loadAndValidateChart(installClient *action.Install, installOpts options.InstallOptions) (*chart.Chart, error) {
// Locate and load the chart
chartPath, err := installClient.ChartPathOptions.LocateChart(installOpts.Chart, hspm.settings)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart", installOpts.Chart).
Err(err).
Msg("Failed to locate chart for helm release installation")
return nil, errors.Wrapf(err, "failed to find the helm chart at the path: %s/%s", installOpts.Repo, installOpts.Chart)
}
chartReq, err := loader.Load(chartPath)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart_path", chartPath).
Err(err).
Msg("Failed to load chart for helm release installation")
return nil, errors.Wrap(err, "failed to load chart for helm release installation")
}
// Check chart dependencies to make sure all are present in /charts
if chartDependencies := chartReq.Metadata.Dependencies; chartDependencies != nil {
if err := action.CheckDependencies(chartReq, chartDependencies); err != nil {
err = errors.Wrap(err, "failed to check chart dependencies for helm release installation")
if !installClient.DependencyUpdate {
return nil, err
}
log.Debug().
Str("context", "HelmClient").
Str("chart", installOpts.Chart).
Msg("Updating chart dependencies for helm release installation")
providers := getter.All(hspm.settings)
manager := &downloader.Manager{
Out: os.Stdout,
ChartPath: chartPath,
Keyring: installClient.ChartPathOptions.Keyring,
SkipUpdate: false,
Getters: providers,
RepositoryConfig: hspm.settings.RepositoryConfig,
RepositoryCache: hspm.settings.RepositoryCache,
Debug: hspm.settings.Debug,
}
if err := manager.Update(); err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart", installOpts.Chart).
Err(err).
Msg("Failed to update chart dependencies for helm release installation")
return nil, errors.Wrap(err, "failed to update chart dependencies for helm release installation")
}
// Reload the chart with the updated Chart.lock file.
if chartReq, err = loader.Load(chartPath); err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart_path", chartPath).
Err(err).
Msg("Failed to reload chart after dependency update for helm release installation")
return nil, errors.Wrap(err, "failed to reload chart after dependency update for helm release installation")
}
}
}
return chartReq, nil
}
// initInstallClient initializes the install client with the given options
// and return the install client.
func initInstallClient(actionConfig *action.Configuration, installOpts options.InstallOptions) (*action.Install, error) {
installClient := action.NewInstall(actionConfig)
installClient.CreateNamespace = true
installClient.DependencyUpdate = true
installClient.ReleaseName = installOpts.Name
installClient.Namespace = installOpts.Namespace
installClient.ChartPathOptions.RepoURL = installOpts.Repo
installClient.Wait = installOpts.Wait
installClient.Timeout = installOpts.Timeout
// Set default values if not specified
if installOpts.Timeout == 0 {
installClient.Timeout = 5 * time.Minute
} else {
installClient.Timeout = installOpts.Timeout
}
if installOpts.Namespace == "" {
installClient.Namespace = "default"
} else {
installClient.Namespace = installOpts.Namespace
}
if installOpts.PostRenderer != "" {
postRenderer, err := postrender.NewExec(installOpts.PostRenderer)
if err != nil {

View file

@ -9,26 +9,6 @@ import (
"github.com/stretchr/testify/assert"
)
func createValuesFile(values string) (string, error) {
file, err := os.CreateTemp("", "helm-values")
if err != nil {
return "", err
}
_, err = file.WriteString(values)
if err != nil {
file.Close()
return "", err
}
err = file.Close()
if err != nil {
return "", err
}
return file.Name(), nil
}
func Test_Install(t *testing.T) {
test.EnsureIntegrationTest(t)
is := assert.New(t)
@ -44,10 +24,14 @@ func Test_Install(t *testing.T) {
Repo: "https://kubernetes.github.io/ingress-nginx",
}
release, err := hspm.Install(installOpts)
hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
release, err := hspm.Upgrade(installOpts)
if release != nil {
defer hspm.Uninstall(options.UninstallOptions{
Name: "test-nginx",
Name: installOpts.Name,
})
}
@ -60,9 +44,8 @@ func Test_Install(t *testing.T) {
t.Run("successfully installs nginx with values", func(t *testing.T) {
// SDK equivalent of: helm install test-nginx-2 --repo https://kubernetes.github.io/ingress-nginx nginx --values /tmp/helm-values3161785816
values, err := createValuesFile("service:\n port: 8081")
values, err := test.CreateValuesFile("service:\n port: 8081")
is.NoError(err, "should create a values file")
defer os.Remove(values)
installOpts := options.InstallOptions{
@ -71,10 +54,15 @@ func Test_Install(t *testing.T) {
Repo: "https://kubernetes.github.io/ingress-nginx",
ValuesFile: values,
}
release, err := hspm.Install(installOpts)
hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
release, err := hspm.Upgrade(installOpts)
if release != nil {
defer hspm.Uninstall(options.UninstallOptions{
Name: "test-nginx-2",
Name: installOpts.Name,
})
}
@ -92,7 +80,12 @@ func Test_Install(t *testing.T) {
Chart: "portainer",
Repo: "https://portainer.github.io/k8s/",
}
release, err := hspm.Install(installOpts)
hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
release, err := hspm.Upgrade(installOpts)
if release != nil {
defer hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
@ -108,9 +101,8 @@ func Test_Install(t *testing.T) {
t.Run("install with values as string", func(t *testing.T) {
// First create a values file since InstallOptions doesn't support values as string directly
values, err := createValuesFile("service:\n port: 8082")
values, err := test.CreateValuesFile("service:\n port: 8082")
is.NoError(err, "should create a values file")
defer os.Remove(values)
// Install with values file
@ -120,10 +112,15 @@ func Test_Install(t *testing.T) {
Repo: "https://kubernetes.github.io/ingress-nginx",
ValuesFile: values,
}
release, err := hspm.Install(installOpts)
hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
release, err := hspm.Upgrade(installOpts)
if release != nil {
defer hspm.Uninstall(options.UninstallOptions{
Name: "test-nginx-3",
Name: installOpts.Name,
})
}
@ -140,11 +137,15 @@ func Test_Install(t *testing.T) {
Repo: "https://kubernetes.github.io/ingress-nginx",
Namespace: "default",
}
release, err := hspm.Install(installOpts)
hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
release, err := hspm.Upgrade(installOpts)
if release != nil {
defer hspm.Uninstall(options.UninstallOptions{
Name: "test-nginx-4",
Namespace: "default",
Name: installOpts.Name,
})
}
@ -159,10 +160,15 @@ func Test_Install(t *testing.T) {
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
_, err := hspm.Install(installOpts)
hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
_, err := hspm.Upgrade(installOpts)
is.Error(err, "should return an error when name is not provided")
is.Equal(err.Error(), "name is required")
// is.Equal(err.Error(), "name is required for helm release installation")
})
t.Run("install with invalid chart", func(t *testing.T) {
@ -172,9 +178,8 @@ func Test_Install(t *testing.T) {
Chart: "non-existent-chart",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
_, err := hspm.Install(installOpts)
_, err := hspm.Upgrade(installOpts)
is.Error(err, "should return error when chart doesn't exist")
is.Equal(err.Error(), "failed to find the helm chart at the path: https://kubernetes.github.io/ingress-nginx/non-existent-chart")
})
t.Run("install with invalid repo", func(t *testing.T) {
@ -184,7 +189,12 @@ func Test_Install(t *testing.T) {
Chart: "nginx",
Repo: "https://non-existent-repo.example.com",
}
_, err := hspm.Install(installOpts)
hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
_, err := hspm.Upgrade(installOpts)
is.Error(err, "should return error when repo doesn't exist")
})
}

View file

@ -26,11 +26,7 @@ func (hspm *HelmSDKPackageManager) List(listOpts options.ListOptions) ([]release
actionConfig := new(action.Configuration)
err := hspm.initActionConfig(actionConfig, listOpts.Namespace, listOpts.KubernetesClusterAccess)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("namespace", listOpts.Namespace).
Err(err).
Msg("Failed to initialize helm configuration")
// error is already logged in initActionConfig
return nil, errors.Wrap(err, "failed to initialize helm configuration")
}

View file

@ -0,0 +1,47 @@
package sdk
import (
"fmt"
"github.com/pkg/errors"
"github.com/portainer/portainer/pkg/libhelm/options"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
)
func (hspm *HelmSDKPackageManager) doesReleaseExist(releaseName, namespace string, clusterAccess *options.KubernetesClusterAccess) (bool, error) {
// Initialize action configuration
actionConfig := new(action.Configuration)
err := hspm.initActionConfig(actionConfig, namespace, clusterAccess)
if err != nil {
// error is already logged in initActionConfig
return false, fmt.Errorf("failed to initialize helm configuration: %w", err)
}
historyClient, err := hspm.initHistoryClient(actionConfig, namespace, clusterAccess)
if err != nil {
// error is already logged in initHistoryClient
return false, fmt.Errorf("failed to initialize helm history client: %w", err)
}
versions, err := historyClient.Run(releaseName)
if errors.Is(err, driver.ErrReleaseNotFound) || isReleaseUninstalled(versions) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("failed to get history: %w", err)
}
return true, nil
}
func isReleaseUninstalled(versions []*release.Release) bool {
return len(versions) > 0 && versions[len(versions)-1].Info.Status == release.StatusUninstalled
}
func (hspm *HelmSDKPackageManager) initHistoryClient(actionConfig *action.Configuration, namespace string, clusterAccess *options.KubernetesClusterAccess) (*action.History, error) {
historyClient := action.NewHistory(actionConfig)
historyClient.Max = 1
return historyClient, nil
}

View file

@ -32,13 +32,11 @@ func (hspm *HelmSDKPackageManager) Show(showOpts options.ShowOptions) ([]byte, e
Str("output_format", string(showOpts.OutputFormat)).
Msg("Showing chart information")
// Initialize action configuration
// Initialize action configuration (no namespace or cluster access needed)
actionConfig := new(action.Configuration)
if err := actionConfig.Init(nil, "", "", func(format string, v ...interface{}) {}); err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to initialize helm configuration")
err := hspm.initActionConfig(actionConfig, "", nil)
if err != nil {
// error is already logged in initActionConfig
return nil, fmt.Errorf("failed to initialize helm configuration: %w", err)
}

View file

@ -21,7 +21,7 @@ func Test_Show(t *testing.T) {
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
release, err := hspm.Install(installOpts)
release, err := hspm.Upgrade(installOpts)
if release != nil || err != nil {
defer hspm.Uninstall(options.UninstallOptions{
Name: "ingress-nginx",

View file

@ -0,0 +1,26 @@
package testutils
import (
"os"
)
// CreateValuesFile creates a temporary file with the given content for testing
func CreateValuesFile(values string) (string, error) {
file, err := os.CreateTemp("", "helm-values")
if err != nil {
return "", err
}
_, err = file.WriteString(values)
if err != nil {
file.Close()
return "", err
}
err = file.Close()
if err != nil {
return "", err
}
return file.Name(), nil
}

View file

@ -26,12 +26,7 @@ func (hspm *HelmSDKPackageManager) Uninstall(uninstallOpts options.UninstallOpti
actionConfig := new(action.Configuration)
err := hspm.initActionConfig(actionConfig, uninstallOpts.Namespace, uninstallOpts.KubernetesClusterAccess)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("release", uninstallOpts.Name).
Str("namespace", uninstallOpts.Namespace).
Err(err).
Msg("Failed to initialize helm configuration")
// error is already logged in initActionConfig
return errors.Wrap(err, "failed to initialize helm configuration")
}

View file

@ -48,7 +48,7 @@ func Test_Uninstall(t *testing.T) {
}
// Install the release
_, err := hspm.Install(installOpts)
_, err := hspm.Upgrade(installOpts)
if err != nil {
t.Logf("Error installing release: %v", err)
t.Skip("Skipping uninstall test because install failed")

161
pkg/libhelm/sdk/upgrade.go Normal file
View file

@ -0,0 +1,161 @@
package sdk
import (
"time"
"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/postrender"
)
// Upgrade implements the HelmPackageManager interface by using the Helm SDK to upgrade a chart.
// If the release does not exist, it will install it instead.
func (hspm *HelmSDKPackageManager) Upgrade(upgradeOpts options.InstallOptions) (*release.Release, error) {
log.Debug().
Str("context", "HelmClient").
Str("chart", upgradeOpts.Chart).
Str("name", upgradeOpts.Name).
Str("namespace", upgradeOpts.Namespace).
Str("repo", upgradeOpts.Repo).
Bool("wait", upgradeOpts.Wait).
Msg("Upgrading Helm chart")
if upgradeOpts.Name == "" {
log.Error().
Str("context", "HelmClient").
Str("chart", upgradeOpts.Chart).
Str("name", upgradeOpts.Name).
Str("namespace", upgradeOpts.Namespace).
Str("repo", upgradeOpts.Repo).
Bool("wait", upgradeOpts.Wait).
Msg("Name is required for helm release upgrade")
return nil, errors.New("name is required for helm release upgrade")
}
// Check if the release exists
exists, err := hspm.doesReleaseExist(upgradeOpts.Name, upgradeOpts.Namespace, upgradeOpts.KubernetesClusterAccess)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("name", upgradeOpts.Name).
Str("namespace", upgradeOpts.Namespace).
Err(err).
Msg("Failed to check if release exists")
return nil, errors.Wrap(err, "failed to check if release exists")
}
// If the release doesn't exist, install it instead
if !exists {
log.Info().
Str("context", "HelmClient").
Str("chart", upgradeOpts.Chart).
Str("name", upgradeOpts.Name).
Str("namespace", upgradeOpts.Namespace).
Msg("Release doesn't exist, installing instead")
return hspm.install(upgradeOpts)
}
// Initialize action configuration with kubernetes config
actionConfig := new(action.Configuration)
err = hspm.initActionConfig(actionConfig, upgradeOpts.Namespace, upgradeOpts.KubernetesClusterAccess)
if err != nil {
// error is already logged in initActionConfig
return nil, errors.Wrap(err, "failed to initialize helm configuration for helm release upgrade")
}
upgradeClient, err := initUpgradeClient(actionConfig, upgradeOpts)
if err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to initialize helm upgrade client for helm release upgrade")
return nil, errors.Wrap(err, "failed to initialize helm upgrade client for helm release upgrade")
}
values, err := hspm.GetHelmValuesFromFile(upgradeOpts.ValuesFile)
if err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to get Helm values from file for helm release upgrade")
return nil, errors.Wrap(err, "failed to get Helm values from file for helm release upgrade")
}
chart, err := hspm.loadAndValidateChartWithPathOptions(&upgradeClient.ChartPathOptions, upgradeOpts.Chart, upgradeOpts.Repo, upgradeClient.DependencyUpdate, "release upgrade")
if err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to load and validate chart for helm release upgrade")
return nil, errors.Wrap(err, "failed to load and validate chart for helm release upgrade")
}
log.Info().
Str("context", "HelmClient").
Str("chart", upgradeOpts.Chart).
Str("name", upgradeOpts.Name).
Str("namespace", upgradeOpts.Namespace).
Msg("Running chart upgrade for helm release")
helmRelease, err := upgradeClient.Run(upgradeOpts.Name, chart, values)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart", upgradeOpts.Chart).
Str("name", upgradeOpts.Name).
Str("namespace", upgradeOpts.Namespace).
Err(err).
Msg("Failed to upgrade helm chart for helm release upgrade")
return nil, errors.Wrap(err, "helm was not able to upgrade the chart for helm release upgrade")
}
return &release.Release{
Name: helmRelease.Name,
Namespace: helmRelease.Namespace,
Chart: release.Chart{
Metadata: &release.Metadata{
Name: helmRelease.Chart.Metadata.Name,
Version: helmRelease.Chart.Metadata.Version,
AppVersion: helmRelease.Chart.Metadata.AppVersion,
},
},
Labels: helmRelease.Labels,
Version: helmRelease.Version,
Manifest: helmRelease.Manifest,
}, nil
}
// initUpgradeClient initializes the upgrade client with the given options
// and return the upgrade client.
func initUpgradeClient(actionConfig *action.Configuration, upgradeOpts options.InstallOptions) (*action.Upgrade, error) {
upgradeClient := action.NewUpgrade(actionConfig)
upgradeClient.DependencyUpdate = true
upgradeClient.Atomic = true
upgradeClient.ChartPathOptions.RepoURL = upgradeOpts.Repo
upgradeClient.Wait = upgradeOpts.Wait
// Set default values if not specified
if upgradeOpts.Timeout == 0 {
upgradeClient.Timeout = 5 * time.Minute
} else {
upgradeClient.Timeout = upgradeOpts.Timeout
}
if upgradeOpts.Namespace == "" {
upgradeOpts.Namespace = "default"
} else {
upgradeClient.Namespace = upgradeOpts.Namespace
}
if upgradeOpts.PostRenderer != "" {
postRenderer, err := postrender.NewExec(upgradeOpts.PostRenderer)
if err != nil {
return nil, errors.Wrap(err, "failed to create post renderer")
}
upgradeClient.PostRenderer = postRenderer
}
return upgradeClient, nil
}

View file

@ -0,0 +1,179 @@
package sdk
import (
"os"
"testing"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/test"
"github.com/stretchr/testify/assert"
)
func TestUpgrade(t *testing.T) {
test.EnsureIntegrationTest(t)
is := assert.New(t)
// Create a new SDK package manager
hspm := NewHelmSDKPackageManager()
t.Run("when no release exists, the chart should be installed", func(t *testing.T) {
// SDK equivalent of: helm upgrade --install test-new-nginx --repo https://kubernetes.github.io/ingress-nginx ingress-nginx
upgradeOpts := options.InstallOptions{
Name: "test-new-nginx",
Namespace: "default",
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
// Ensure the release doesn't exist before test
hspm.Uninstall(options.UninstallOptions{
Name: upgradeOpts.Name,
})
release, err := hspm.Upgrade(upgradeOpts)
if release != nil {
defer hspm.Uninstall(options.UninstallOptions{
Name: upgradeOpts.Name,
})
}
is.NoError(err, "should successfully install release via upgrade")
is.NotNil(release, "should return non-nil release")
is.Equal(upgradeOpts.Name, release.Name, "release name should match")
is.Equal(1, release.Version, "release version should be 1 for new install")
is.NotEmpty(release.Manifest, "release manifest should not be empty")
// Cleanup
defer hspm.Uninstall(options.UninstallOptions{
Name: upgradeOpts.Name,
})
})
t.Run("when release exists, the chart should be upgraded", func(t *testing.T) {
// First install a release
installOpts := options.InstallOptions{
Name: "test-upgrade-nginx",
Chart: "ingress-nginx",
Namespace: "default",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
// Ensure the release doesn't exist before test
hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
release, err := hspm.Upgrade(installOpts)
defer hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
is.NoError(err, "should successfully install release")
is.NotNil(release, "should return non-nil release")
// Upgrade the release with the same options
upgradedRelease, err := hspm.Upgrade(installOpts)
is.NoError(err, "should successfully upgrade release")
is.NotNil(upgradedRelease, "should return non-nil release")
is.Equal("test-upgrade-nginx", upgradedRelease.Name, "release name should match")
is.Equal(2, upgradedRelease.Version, "release version should be incremented to 2")
is.NotEmpty(upgradedRelease.Manifest, "release manifest should not be empty")
})
t.Run("should be able to upgrade with override values", func(t *testing.T) {
// First install a release
installOpts := options.InstallOptions{
Name: "test-values-nginx",
Chart: "ingress-nginx",
Namespace: "default",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
// Ensure the release doesn't exist before test
hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
release, err := hspm.Upgrade(installOpts) // Cleanup
defer hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
is.NoError(err, "should successfully install release")
is.NotNil(release, "should return non-nil release")
// Create values file
values, err := test.CreateValuesFile("service:\n port: 8083")
is.NoError(err, "should create a values file")
defer os.Remove(values)
// Now upgrade with values
upgradeOpts := options.InstallOptions{
Name: "test-values-nginx",
Chart: "ingress-nginx",
Namespace: "default",
Repo: "https://kubernetes.github.io/ingress-nginx",
ValuesFile: values,
}
upgradedRelease, err := hspm.Upgrade(upgradeOpts)
is.NoError(err, "should successfully upgrade release with values")
is.NotNil(upgradedRelease, "should return non-nil release")
is.Equal("test-values-nginx", upgradedRelease.Name, "release name should match")
is.Equal(2, upgradedRelease.Version, "release version should be incremented to 2")
is.NotEmpty(upgradedRelease.Manifest, "release manifest should not be empty")
})
t.Run("should give an error if the override values are invalid", func(t *testing.T) {
// First install a release
installOpts := options.InstallOptions{
Name: "test-invalid-values",
Chart: "ingress-nginx",
Namespace: "default",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
// Ensure the release doesn't exist before test
hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
release, err := hspm.Upgrade(installOpts)
defer hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
is.NoError(err, "should successfully install release")
is.NotNil(release, "should return non-nil release")
// Create invalid values file
values, err := test.CreateValuesFile("this is not valid yaml")
is.NoError(err, "should create a values file")
defer os.Remove(values)
// Now upgrade with invalid values
upgradeOpts := options.InstallOptions{
Name: "test-invalid-values",
Chart: "ingress-nginx",
Namespace: "default",
Repo: "https://kubernetes.github.io/ingress-nginx",
ValuesFile: values,
}
_, err = hspm.Upgrade(upgradeOpts)
is.Error(err, "should return error with invalid values")
})
t.Run("should return error when name is not provided", func(t *testing.T) {
upgradeOpts := options.InstallOptions{
Chart: "ingress-nginx",
Namespace: "default",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
_, err := hspm.Upgrade(upgradeOpts)
is.Error(err, "should return an error when name is not provided")
is.Equal("name is required for helm release upgrade", err.Error(), "should return correct error message")
})
}

View file

@ -73,6 +73,11 @@ func (hpm *helmMockPackageManager) Install(installOpts options.InstallOptions) (
return newMockRelease(releaseElement), nil
}
// Upgrade a helm chart (not thread safe)
func (hpm *helmMockPackageManager) Upgrade(upgradeOpts options.InstallOptions) (*release.Release, error) {
return hpm.Install(upgradeOpts)
}
// Show values/readme/chart etc
func (hpm *helmMockPackageManager) Show(showOpts options.ShowOptions) ([]byte, error) {
switch showOpts.OutputFormat {

View file

@ -0,0 +1,26 @@
package test
import (
"os"
)
// CreateValuesFile creates a temporary file with the given content for testing
func CreateValuesFile(values string) (string, error) {
file, err := os.CreateTemp("", "helm-values")
if err != nil {
return "", err
}
_, err = file.WriteString(values)
if err != nil {
file.Close()
return "", err
}
err = file.Close()
if err != nil {
return "", err
}
return file.Name(), nil
}

View file

@ -12,7 +12,7 @@ type HelmPackageManager interface {
Show(showOpts options.ShowOptions) ([]byte, error)
SearchRepo(searchRepoOpts options.SearchRepoOptions) ([]byte, error)
List(listOpts options.ListOptions) ([]release.ReleaseElement, error)
Install(installOpts options.InstallOptions) (*release.Release, error)
Upgrade(upgradeOpts options.InstallOptions) (*release.Release, error)
Uninstall(uninstallOpts options.UninstallOptions) error
}