|
| 1 | +/* |
| 2 | +Copyright 2023 The Radius Authors. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package kubernetes |
| 18 | + |
| 19 | +import ( |
| 20 | + "context" |
| 21 | + "fmt" |
| 22 | + |
| 23 | + "github.com/Masterminds/semver/v3" |
| 24 | + "github.com/radius-project/radius/pkg/cli/cmd/commonflags" |
| 25 | + "github.com/radius-project/radius/pkg/cli/framework" |
| 26 | + "github.com/radius-project/radius/pkg/cli/helm" |
| 27 | + "github.com/radius-project/radius/pkg/cli/output" |
| 28 | + "github.com/spf13/cobra" |
| 29 | +) |
| 30 | + |
| 31 | +// Updated NewCommand remains unchanged... |
| 32 | +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { |
| 33 | + runner := NewRunner(factory) |
| 34 | + cmd := &cobra.Command{ |
| 35 | + Use: "kubernetes", |
| 36 | + Short: "Upgrades Radius on a Kubernetes cluster", |
| 37 | + Long: `Upgrade Radius on a Kubernetes cluster using the Radius Helm chart. |
| 38 | +By default 'rad upgrade kubernetes' will upgrade Radius to the latest version available. |
| 39 | + |
| 40 | +Before performing the upgrade, a snapshot of the current installation is taken so that it can be restored if necessary.`, |
| 41 | + Example: ` |
| 42 | +# Upgrade to the latest version |
| 43 | +rad upgrade kubernetes |
| 44 | +
|
| 45 | +# Upgrade with custom configuration values |
| 46 | +rad upgrade kubernetes --version v0.44.0 --set global.monitoring.enabled=true |
| 47 | +`, |
| 48 | + Args: cobra.ExactArgs(0), |
| 49 | + RunE: framework.RunCommand(runner), |
| 50 | + } |
| 51 | + |
| 52 | + commonflags.AddKubeContextFlagVar(cmd, &runner.KubeContext) |
| 53 | + cmd.Flags().StringVar(&runner.Version, "version", "", "Specify a version to upgrade to (default uses the latest version)") |
| 54 | + cmd.Flags().IntVar(&runner.Timeout, "timeout", 300, "Timeout in seconds for the upgrade operation") |
| 55 | + cmd.Flags().StringArrayVar(&runner.Set, "set", []string{}, "Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") |
| 56 | + cmd.Flags().StringArrayVar(&runner.SetFile, "set-file", []string{}, "Set values from files on the command line") |
| 57 | + return cmd, runner |
| 58 | +} |
| 59 | + |
| 60 | +// Runner is the Runner implementation for the upgrade command. |
| 61 | +type Runner struct { |
| 62 | + Helm helm.Interface |
| 63 | + Output output.Interface |
| 64 | + |
| 65 | + KubeContext string |
| 66 | + Version string |
| 67 | + DryRun bool |
| 68 | + Timeout int |
| 69 | + Set []string |
| 70 | + SetFile []string |
| 71 | +} |
| 72 | + |
| 73 | +// NewRunner creates a new Runner. |
| 74 | +func NewRunner(factory framework.Factory) *Runner { |
| 75 | + return &Runner{ |
| 76 | + Helm: factory.GetHelmInterface(), |
| 77 | + Output: factory.GetOutput(), |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +/* |
| 82 | +Validate required flags. |
| 83 | +• Ensure that the --version flag is provided (since downgrades aren’t supported). |
| 84 | +• Also handle other flags (like --timeout, --set, etc.). |
| 85 | +
|
| 86 | +Check if Radius is installed. |
| 87 | +• Use the Helm client to query the current state. |
| 88 | +• If not installed, abort with an informative message. |
| 89 | +
|
| 90 | +Retrieve chart versions. |
| 91 | +• Query the Helm repository for available chart versions. |
| 92 | +• Identify the list of available versions and determine the latest version. |
| 93 | +• Optionally, log these available versions for reference. |
| 94 | +
|
| 95 | +Compare version numbers. |
| 96 | +• Retrieve the current version installed on the cluster. |
| 97 | +• Check if the target (wanted) version is higher than the current version. |
| 98 | +• If the target is equal to or lower than the current version, abort the upgrade. |
| 99 | +
|
| 100 | +Keep a global flag for in-progress upgrade. |
| 101 | +• Set a global flag to indicate that an upgrade is in progress. |
| 102 | +• This can be used to prevent multiple concurrent upgrades and other data-changing operations. |
| 103 | +• This flag should be cleared after the upgrade process is completed. |
| 104 | +
|
| 105 | +Snapshot the data. |
| 106 | +• Before making any live changes, automatically or via a prompt, trigger a snapshot (or backup) of your data (etcd, etc.). |
| 107 | +• This safeguards the installation in case a rollback is needed. |
| 108 | +
|
| 109 | +Perform the upgrade. |
| 110 | +• Initiate the Helm upgrade process. |
| 111 | +• Pass along the appropriate configuration (including timeout, value overrides, etc.) |
| 112 | +
|
| 113 | +Rollback if necessary. |
| 114 | +• If the upgrade fails, use Helm’s rollback capabilities to revert to the previous version. |
| 115 | +• Include additional logging and error messages to guide the user. |
| 116 | +
|
| 117 | +Post-upgrade validation. |
| 118 | +• Verify that the new version is running correctly and all critical components are healthy. |
| 119 | +*/ |
| 120 | + |
| 121 | +// Run executes the upgrade flow. |
| 122 | +func (r *Runner) Run(ctx context.Context) error { |
| 123 | + cliOptions := helm.CLIClusterOptions{ |
| 124 | + Radius: helm.ChartOptions{ |
| 125 | + SetArgs: r.Set, |
| 126 | + SetFileArgs: r.SetFile, |
| 127 | + }, |
| 128 | + } |
| 129 | + |
| 130 | + // Check if Radius is installed. |
| 131 | + state, err := r.Helm.CheckRadiusInstall(r.KubeContext) |
| 132 | + if err != nil { |
| 133 | + return err |
| 134 | + } |
| 135 | + if !state.RadiusInstalled { |
| 136 | + r.Output.LogInfo("No existing Radius installation found. Use 'rad install kubernetes' to install.") |
| 137 | + return nil |
| 138 | + } |
| 139 | + |
| 140 | + // Get Control Plane version (not CLI version) |
| 141 | + currentControlPlaneVersion := state.RadiusVersion |
| 142 | + r.Output.LogInfo("Current Control Plane version: %s", currentControlPlaneVersion) |
| 143 | + |
| 144 | + // Determine desired version |
| 145 | + desiredVersion := r.Version |
| 146 | + if desiredVersion == "" { |
| 147 | + // Default to latest |
| 148 | + desiredVersion = "latest" |
| 149 | + r.Output.LogInfo("No version specified, upgrading to latest version") |
| 150 | + } else { |
| 151 | + // Validate the desired version is a valid upgrade |
| 152 | + valid, message, validationErr := r.isValidUpgradeVersion(currentControlPlaneVersion, desiredVersion) |
| 153 | + if validationErr != nil { |
| 154 | + return fmt.Errorf("error validating version: %w", validationErr) |
| 155 | + } |
| 156 | + |
| 157 | + if !valid { |
| 158 | + r.Output.LogInfo("Invalid upgrade version: %s", message) |
| 159 | + return fmt.Errorf("invalid upgrade version: %s", message) |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + r.Output.LogInfo("Upgrading from version %s to %s", currentControlPlaneVersion, desiredVersion) |
| 164 | + |
| 165 | + // Set the version in cluster options |
| 166 | + if desiredVersion != "latest" { |
| 167 | + cliOptions.Radius.ChartVersion = desiredVersion |
| 168 | + } |
| 169 | + |
| 170 | + // Take snapshot of the current state before upgrade |
| 171 | + r.Output.LogInfo("Taking snapshot of the current installation...") |
| 172 | + // Implement snapshot logic here |
| 173 | + |
| 174 | + clusterOptions := helm.PopulateDefaultClusterOptions(cliOptions) |
| 175 | + _, err = r.Helm.UpgradeRadius(ctx, clusterOptions, r.KubeContext) |
| 176 | + if err != nil { |
| 177 | + r.Output.LogInfo("Rolling back to previous state...") |
| 178 | + // Implement rollback logic here |
| 179 | + return err |
| 180 | + } |
| 181 | + |
| 182 | + r.Output.LogInfo("Upgrade completed successfully.") |
| 183 | + return nil |
| 184 | +} |
| 185 | + |
| 186 | +// // takeSnapshot uses the data store snapshot functionality to back up the current state. |
| 187 | +// func (r *Runner) takeSnapshot(ctx context.Context) ([]byte, error) { |
| 188 | +// return []byte("snapshot data"), nil |
| 189 | +// } |
| 190 | + |
| 191 | +// // performRollback uses the snapshot data to restore the previous state. |
| 192 | +// func (r *Runner) performRollback(ctx context.Context, snapshot []byte) error { |
| 193 | +// return nil |
| 194 | +// } |
| 195 | + |
| 196 | +// Validate runs any validations needed for the command. |
| 197 | +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { |
| 198 | + return nil |
| 199 | +} |
| 200 | + |
| 201 | +// isValidUpgradeVersion checks if the target version is a valid upgrade from the current version |
| 202 | +func (r *Runner) isValidUpgradeVersion(currentVersion, targetVersion string) (bool, string, error) { |
| 203 | + // Handle "latest" as a special case |
| 204 | + if targetVersion == "latest" { |
| 205 | + return true, "", nil |
| 206 | + } |
| 207 | + |
| 208 | + // Ensure both versions have 'v' prefix for semver parsing |
| 209 | + if len(currentVersion) > 0 && currentVersion[0] != 'v' { |
| 210 | + currentVersion = "v" + currentVersion |
| 211 | + } |
| 212 | + if len(targetVersion) > 0 && targetVersion[0] != 'v' { |
| 213 | + targetVersion = "v" + targetVersion |
| 214 | + } |
| 215 | + |
| 216 | + // Parse versions using semver library |
| 217 | + current, err := semver.NewVersion(currentVersion) |
| 218 | + if err != nil { |
| 219 | + return false, "", fmt.Errorf("invalid current version format: %w", err) |
| 220 | + } |
| 221 | + |
| 222 | + target, err := semver.NewVersion(targetVersion) |
| 223 | + if err != nil { |
| 224 | + return false, "", fmt.Errorf("invalid target version format: %w", err) |
| 225 | + } |
| 226 | + |
| 227 | + // Check if versions are the same |
| 228 | + if current.Equal(target) { |
| 229 | + return false, "Target version is the same as current version", nil |
| 230 | + } |
| 231 | + |
| 232 | + // Check if downgrade attempt |
| 233 | + if target.LessThan(current) { |
| 234 | + return false, "Downgrading is not supported", nil |
| 235 | + } |
| 236 | + |
| 237 | + // Get the next expected version (increment minor version) |
| 238 | + expectedNextMinor := semver.New(current.Major(), current.Minor()+1, 0, "", "") |
| 239 | + |
| 240 | + // Special case: major version increment (e.g., 0.x -> 1.0) |
| 241 | + if target.Major() > current.Major() { |
| 242 | + if target.Major() == current.Major()+1 && target.Minor() == 0 && target.Patch() == 0 { |
| 243 | + return true, "", nil |
| 244 | + } |
| 245 | + return false, fmt.Sprintf("Skipping multiple major versions not supported. Expected next major version: %d.0.0", current.Major()+1), nil |
| 246 | + } |
| 247 | + |
| 248 | + // Allow increment of minor version by exactly 1 |
| 249 | + if target.Major() == current.Major() && target.Minor() == current.Minor()+1 { |
| 250 | + return true, "", nil |
| 251 | + } |
| 252 | + |
| 253 | + return false, fmt.Sprintf("Only incremental version upgrades are supported. Expected next version: %s", expectedNextMinor), nil |
| 254 | +} |
0 commit comments