Skip to content

Commit 12d281e

Browse files
committed
Adding 'rad upgrade kubernetes' command
Signed-off-by: ytimocin <[email protected]>
1 parent c287e7d commit 12d281e

File tree

4 files changed

+291
-1
lines changed

4 files changed

+291
-1
lines changed

cmd/rad/cmd/root.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ import (
7070
"github.com/radius-project/radius/pkg/cli/cmd/run"
7171
"github.com/radius-project/radius/pkg/cli/cmd/uninstall"
7272
uninstall_kubernetes "github.com/radius-project/radius/pkg/cli/cmd/uninstall/kubernetes"
73+
upgrade "github.com/radius-project/radius/pkg/cli/cmd/upgrade"
74+
upgrade_kubernetes "github.com/radius-project/radius/pkg/cli/cmd/upgrade/kubernetes"
7375
workspace_create "github.com/radius-project/radius/pkg/cli/cmd/workspace/create"
7476
workspace_delete "github.com/radius-project/radius/pkg/cli/cmd/workspace/delete"
7577
workspace_list "github.com/radius-project/radius/pkg/cli/cmd/workspace/list"
@@ -371,6 +373,12 @@ func initSubCommands() {
371373

372374
uninstallKubernetesCmd, _ := uninstall_kubernetes.NewCommand(framework)
373375
uninstallCmd.AddCommand(uninstallKubernetesCmd)
376+
377+
upgradeCmd := upgrade.NewCommand()
378+
RootCmd.AddCommand(upgradeCmd)
379+
380+
upgradeKubernetesCmd, _ := upgrade_kubernetes.NewCommand(framework)
381+
upgradeCmd.AddCommand(upgradeKubernetesCmd)
374382
}
375383

376384
// The dance we do with config is kinda complex. We want commands to be able to retrieve a config (*viper.Viper)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ require (
198198
github.com/BurntSushi/toml v1.4.0 // indirect
199199
github.com/MakeNowJust/heredoc v1.0.0 // indirect
200200
github.com/Masterminds/goutils v1.1.1 // indirect
201-
github.com/Masterminds/semver/v3 v3.3.0 // indirect
201+
github.com/Masterminds/semver/v3 v3.3.0
202202
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
203203
github.com/Masterminds/squirrel v1.5.4 // indirect
204204
github.com/ProtonMail/go-crypto v1.1.5 // indirect
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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+
}

pkg/cli/cmd/upgrade/upgrade.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 upgrade
18+
19+
import "github.com/spf13/cobra"
20+
21+
// NewCommand returns a new cobra command for `rad upgrade`.
22+
func NewCommand() *cobra.Command {
23+
return &cobra.Command{
24+
Use: "upgrade",
25+
Short: "Upgrades Radius for a given platform",
26+
Long: `Upgrades Radius for a given platform`,
27+
}
28+
}

0 commit comments

Comments
 (0)