mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 05:19:39 +02:00
feat(helm): use helm upgrade for install [r8s-258] (#568)
This commit is contained in:
parent
e68bd53e30
commit
0ebfe047d1
19 changed files with 613 additions and 150 deletions
|
@ -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
87
pkg/libhelm/sdk/common.go
Normal 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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
47
pkg/libhelm/sdk/release.go
Normal file
47
pkg/libhelm/sdk/release.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
26
pkg/libhelm/sdk/testutils/values.go
Normal file
26
pkg/libhelm/sdk/testutils/values.go
Normal 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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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
161
pkg/libhelm/sdk/upgrade.go
Normal 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
|
||||
}
|
179
pkg/libhelm/sdk/upgrade_test.go
Normal file
179
pkg/libhelm/sdk/upgrade_test.go
Normal 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")
|
||||
})
|
||||
}
|
|
@ -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 {
|
||||
|
|
26
pkg/libhelm/test/values.go
Normal file
26
pkg/libhelm/test/values.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue