diff --git a/pkg/cli/cmd/upgrade/kubernetes/kubernetes.go b/pkg/cli/cmd/upgrade/kubernetes/kubernetes.go index 3846b0f7df..6d70bb0472 100644 --- a/pkg/cli/cmd/upgrade/kubernetes/kubernetes.go +++ b/pkg/cli/cmd/upgrade/kubernetes/kubernetes.go @@ -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 @@ -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 @@ -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 diff --git a/pkg/cli/helm/helmclient.go b/pkg/cli/helm/helmclient.go index cd88e3b6ef..2fae70fc95 100644 --- a/pkg/cli/helm/helmclient.go +++ b/pkg/cli/helm/helmclient.go @@ -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) } diff --git a/pkg/cli/helm/helmclient_test.go b/pkg/cli/helm/helmclient_test.go index 2b01bdf175..2effdc6b02 100644 --- a/pkg/cli/helm/helmclient_test.go +++ b/pkg/cli/helm/helmclient_test.go @@ -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) { @@ -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") + }) +}