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
17 changes: 16 additions & 1 deletion pkg/cli/cmd/upgrade/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) {
This command upgrades the Radius control plane in the cluster associated with the active workspace.
To upgrade Radius in a different cluster, switch to the appropriate workspace first using 'rad workspace switch'.

The upgrade process preserves your existing Helm chart values (such as Azure Workload Identity settings,
database configuration, and custom container registries). The upgrade merges these existing values with
any new values specified via --set or --set-file flags, allowing you to override specific settings
without losing your configuration.

The upgrade process includes preflight checks to ensure the cluster is ready for upgrade.
Preflight checks include:
- Kubernetes connectivity and permissions
Expand All @@ -52,6 +57,7 @@ Preflight checks include:
Radius is installed in the 'radius-system' namespace. For more information visit https://docs.radapp.io/concepts/technical/architecture/.
`,
Example: `# Upgrade Radius in the cluster of the active workspace
# Existing values (e.g., from initial install) are automatically preserved
rad upgrade kubernetes

# Check which workspace is active
Expand All @@ -61,9 +67,18 @@ rad workspace show
rad workspace switch myworkspace
rad upgrade kubernetes

# Upgrade Radius with custom configuration
# Upgrade Radius and override a specific value
# All other existing values from the previous installation are preserved
rad upgrade kubernetes --set key=value

# Example: If you installed with Azure Workload Identity enabled:
# rad install kubernetes --set global.azureWorkloadIdentity.enabled=true
# Then upgrade without repeating the flag - the setting is preserved:
rad upgrade kubernetes

# You can still override specific values during upgrade:
rad upgrade kubernetes --set global.imageTag=0.48

# Upgrade Radius with a custom container registry
# Images will be pulled as: myregistry.azurecr.io/controller, myregistry.azurecr.io/ucpd, etc.
rad upgrade kubernetes --set global.imageRegistry=myregistry.azurecr.io
Expand Down
2 changes: 2 additions & 0 deletions pkg/cli/helm/helmclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,14 @@ func (client *HelmClientImpl) RunHelmInstall(helmConf *helm.Configuration, helmC

// RunHelmUpgrade upgrades an existing Helm release with a new chart version or configuration.
// It recreates pods to ensure the new configuration is applied and optionally waits for the deployment to be ready.
// The upgrade reuses existing release values and merges them with any new values provided in helmChart.Values.
func (client *HelmClientImpl) RunHelmUpgrade(helmConf *helm.Configuration, helmChart *chart.Chart, releaseName, namespace string, wait bool) (*release.Release, error) {
upgradeClient := helm.NewUpgrade(helmConf)
upgradeClient.Namespace = namespace
upgradeClient.Wait = wait
upgradeClient.Timeout = upgradeTimeout
upgradeClient.Recreate = true
upgradeClient.ReuseValues = true

return upgradeClient.Run(releaseName, helmChart, helmChart.Values)
}
Expand Down
121 changes: 121 additions & 0 deletions pkg/cli/helm/helmclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,19 @@ limitations under the License.
package helm

import (
"io"
"testing"
"time"

"github.com/stretchr/testify/require"
helm "helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/registry"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver"
)

func TestHelmClientImpl_RunHelmHistory(t *testing.T) {
Expand Down Expand Up @@ -105,3 +113,116 @@ func TestHelmClient_MockCompatibility(t *testing.T) {
var rollbackFunc func(*helm.Configuration, string, int, bool) error = client.RunHelmRollback
require.NotNil(t, rollbackFunc)
}

// TestHelmClient_UpgradeReusesValues verifies that RunHelmUpgrade preserves existing values
// by using ReuseValues=true. This test validates the fix for issue #11218 where upgrades
// were resetting values to chart defaults.
func TestHelmClient_UpgradeReusesValues(t *testing.T) {
t.Run("upgrade preserves existing values and merges new values", func(t *testing.T) {
// This is an integration test that validates the actual behavior of RunHelmUpgrade
// with ReuseValues=true by simulating an upgrade with Helm's in-memory storage.

// Test data representing values from previous install/upgrade
existingValues := map[string]interface{}{
"global": map[string]interface{}{
"azureWorkloadIdentity": map[string]interface{}{
"enabled": true,
},
"imageRegistry": "myregistry.azurecr.io",
},
"database": map[string]interface{}{
"enabled": true,
},
}

// New values being applied in this upgrade
newValues := map[string]interface{}{
"global": map[string]interface{}{
"imageTag": "0.48.0",
},
}

// Expected merged result: existing values preserved + new values applied
expectedValues := map[string]interface{}{
"global": map[string]interface{}{
"azureWorkloadIdentity": map[string]interface{}{
"enabled": true,
},
"imageRegistry": "myregistry.azurecr.io",
"imageTag": "0.48.0",
},
"database": map[string]interface{}{
"enabled": true,
},
}

// Create an in-memory Helm configuration for testing (similar to Helm's own tests)
registryClient, err := registry.NewClient()
require.NoError(t, err, "Failed to create registry client")

cfg := &helm.Configuration{
Releases: storage.Init(driver.NewMemory()),
KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard},
Capabilities: chartutil.DefaultCapabilities,
RegistryClient: registryClient,
Log: func(format string, v ...interface{}) {},
}

// Create and store an initial release with existing values
initialRelease := &release.Release{
Name: "test-release",
Namespace: "test-namespace",
Version: 1,
Config: existingValues,
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
},
},
Info: &release.Info{
Status: release.StatusDeployed,
},
}
err = cfg.Releases.Create(initialRelease)
require.NoError(t, err, "Failed to create initial release")

// Create a chart with new values
upgradeChart := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.1.0",
},
Values: newValues,
}

// Execute the upgrade using our RunHelmUpgrade implementation
client := &HelmClientImpl{}
upgradedRelease, err := client.RunHelmUpgrade(cfg, upgradeChart, "test-release", "test-namespace", false)
require.NoError(t, err, "RunHelmUpgrade should succeed")
require.NotNil(t, upgradedRelease, "Upgraded release should not be nil")

// Verify the release was upgraded
require.Equal(t, 2, upgradedRelease.Version, "Release version should be incremented")
require.Equal(t, release.StatusDeployed, upgradedRelease.Info.Status, "Release should be deployed")

// Verify that existing values were preserved and new values were merged
require.Equal(t, expectedValues, upgradedRelease.Config,
"Upgraded release should preserve existing values and merge new values")

// Specifically verify key values that should be preserved (issue #11218)
globalMap, ok := upgradedRelease.Config["global"].(map[string]interface{})
require.True(t, ok, "global key should exist and be a map")

azureWIMap, ok := globalMap["azureWorkloadIdentity"].(map[string]interface{})
require.True(t, ok, "global.azureWorkloadIdentity should exist")
require.Equal(t, true, azureWIMap["enabled"], "azureWorkloadIdentity.enabled should be preserved")

require.Equal(t, "myregistry.azurecr.io", globalMap["imageRegistry"], "imageRegistry should be preserved")
require.Equal(t, "0.48.0", globalMap["imageTag"], "imageTag should be set from new values")

dbMap, ok := upgradedRelease.Config["database"].(map[string]interface{})
require.True(t, ok, "database key should exist and be a map")
require.Equal(t, true, dbMap["enabled"], "database.enabled should be preserved")
})
}