diff --git a/.github/workflows/functional-test.yaml b/.github/workflows/functional-test.yaml index 9d595d50ea0..d5c11f43d98 100644 --- a/.github/workflows/functional-test.yaml +++ b/.github/workflows/functional-test.yaml @@ -414,8 +414,8 @@ jobs: echo "*** Installing Radius to Kubernetes ***" rad install kubernetes \ - --chart ${{ env.RADIUS_CHART_LOCATION }} --tag ${{ env.REL_VERSION }} \ - --set rp.image=${{ env.CACHE_REGISTRY }}/appcore-rp,rp.tag=${{ env.REL_VERSION }},ucp.image=${{ env.CACHE_REGISTRY }}/ucpd,ucp.tag=${{ env.REL_VERSION }},ucp.provider.aws.region=${{ env.AWS_REGION }} + --chart ${{ env.RADIUS_CHART_LOCATION }} \ + --set rp.image=${{ env.CACHE_REGISTRY }}/appcore-rp,rp.tag=${{ env.REL_VERSION }},ucp.image=${{ env.CACHE_REGISTRY }}/ucpd,ucp.tag=${{ env.REL_VERSION }} echo "*** Create workspace, group and environment for test ***" rad workspace create kubernetes diff --git a/cmd/rad/cmd/installKubernetes.go b/cmd/rad/cmd/installKubernetes.go deleted file mode 100644 index 4bc38f57c35..00000000000 --- a/cmd/rad/cmd/installKubernetes.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -Copyright 2023 The Radius Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cmd - -import ( - "github.com/project-radius/radius/pkg/cli/helm" - "github.com/project-radius/radius/pkg/cli/setup" - "github.com/spf13/cobra" -) - -var installKubernetesCmd = &cobra.Command{ - Use: "kubernetes", - Short: "Installs radius onto a kubernetes cluster", - Long: `Installs radius onto a kubernetes cluster`, - RunE: installKubernetes, -} - -func init() { - installCmd.AddCommand(installKubernetesCmd) - installKubernetesCmd.PersistentFlags().BoolP("interactive", "i", false, "Collect values for required command arguments through command line interface prompts") - installKubernetesCmd.Flags().String("kubecontext", "", "the Kubernetes context to use, will use the default if unset") - setup.RegisterPersistentChartArgs(installKubernetesCmd) -} - -func installKubernetes(cmd *cobra.Command, args []string) error { - // It's ok if this is blank. - kubeContext, err := cmd.Flags().GetString("kubecontext") - if err != nil { - return err - } - - chartArgs, err := setup.ParseChartArgs(cmd) - if err != nil { - return err - } - - cliOptions := helm.CLIClusterOptions{ - Radius: helm.RadiusOptions{ - Reinstall: chartArgs.Reinstall, - ChartPath: chartArgs.ChartPath, - Values: chartArgs.Values, - }, - } - - clusterOptions := helm.PopulateDefaultClusterOptions(cliOptions) - - _, err = helm.Install(cmd.Context(), clusterOptions, kubeContext) - if err != nil { - return err - } - - return nil -} diff --git a/cmd/rad/cmd/root.go b/cmd/rad/cmd/root.go index c446a2e3032..7b97682becf 100644 --- a/cmd/rad/cmd/root.go +++ b/cmd/rad/cmd/root.go @@ -45,6 +45,8 @@ import ( env_show "github.com/project-radius/radius/pkg/cli/cmd/env/show" env_update "github.com/project-radius/radius/pkg/cli/cmd/env/update" group "github.com/project-radius/radius/pkg/cli/cmd/group" + "github.com/project-radius/radius/pkg/cli/cmd/install" + install_kubernetes "github.com/project-radius/radius/pkg/cli/cmd/install/kubernetes" "github.com/project-radius/radius/pkg/cli/cmd/radinit" recipe_list "github.com/project-radius/radius/pkg/cli/cmd/recipe/list" recipe_register "github.com/project-radius/radius/pkg/cli/cmd/recipe/register" @@ -54,6 +56,8 @@ import ( resource_list "github.com/project-radius/radius/pkg/cli/cmd/resource/list" resource_show "github.com/project-radius/radius/pkg/cli/cmd/resource/show" "github.com/project-radius/radius/pkg/cli/cmd/run" + "github.com/project-radius/radius/pkg/cli/cmd/uninstall" + uninstall_kubernetes "github.com/project-radius/radius/pkg/cli/cmd/uninstall/kubernetes" workspace_create "github.com/project-radius/radius/pkg/cli/cmd/workspace/create" workspace_delete "github.com/project-radius/radius/pkg/cli/cmd/workspace/delete" workspace_list "github.com/project-radius/radius/pkg/cli/cmd/workspace/list" @@ -272,6 +276,18 @@ func initSubCommands() { bicepPublishCmd, _ := bicep_publish.NewCommand(framework) bicepCmd.AddCommand(bicepPublishCmd) + + installCmd := install.NewCommand() + RootCmd.AddCommand(installCmd) + + installKubernetesCmd, _ := install_kubernetes.NewCommand(framework) + installCmd.AddCommand(installKubernetesCmd) + + uninstallCmd := uninstall.NewCommand() + RootCmd.AddCommand(uninstallCmd) + + uninstallKubernetesCmd, _ := uninstall_kubernetes.NewCommand(framework) + uninstallCmd.AddCommand(uninstallKubernetesCmd) } // The dance we do with config is kinda complex. We want commands to be able to retrieve a config (*viper.Viper) diff --git a/cmd/rad/cmd/uninstallKubernetes.go b/cmd/rad/cmd/uninstallKubernetes.go deleted file mode 100644 index dc0d6cb7d77..00000000000 --- a/cmd/rad/cmd/uninstallKubernetes.go +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2023 The Radius Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cmd - -import ( - "github.com/project-radius/radius/pkg/cli/setup" - "github.com/spf13/cobra" -) - -var uninstallKubernetesCmd = &cobra.Command{ - Use: "kubernetes", - Short: "Uninstall Radius from Kubernetes Cluster", - Long: `Uninstall Radius from Kubernetes Cluster`, - RunE: uninstallKubernetes, -} - -func init() { - uninstallCmd.AddCommand(uninstallKubernetesCmd) - - uninstallKubernetesCmd.Flags().String("kubecontext", "", "the Kubernetes context to use, will use the default if unset") -} - -func uninstallKubernetes(cmd *cobra.Command, args []string) error { - kubeContext, err := cmd.Flags().GetString("kubecontext") - if err != nil { - return err - } - - // It's OK for KubeContext to be blank. - err = setup.Uninstall(cmd.Context(), kubeContext) - if err != nil { - return err - } - - return nil -} diff --git a/pkg/cli/cmd/commonflags/flags.go b/pkg/cli/cmd/commonflags/flags.go index da86629a719..4ebbcd7a4cc 100644 --- a/pkg/cli/cmd/commonflags/flags.go +++ b/pkg/cli/cmd/commonflags/flags.go @@ -107,3 +107,7 @@ func AddAWSRegionFlag(cmd *cobra.Command) { func AddAWSAccountFlag(cmd *cobra.Command) { cmd.Flags().String(AWSAccountIdFlag, "", "The account ID where AWS resources will be deployed") } + +func AddKubeContextFlagVar(cmd *cobra.Command, ref *string) { + cmd.Flags().StringVar(ref, "kubecontext", "", "The Kubernetes context to use, will use the default if unset") +} diff --git a/cmd/rad/cmd/uninstall.go b/pkg/cli/cmd/install/install.go similarity index 66% rename from cmd/rad/cmd/uninstall.go rename to pkg/cli/cmd/install/install.go index 6ae8b1d3bb1..163af485482 100644 --- a/cmd/rad/cmd/uninstall.go +++ b/pkg/cli/cmd/install/install.go @@ -14,18 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cmd +package install -import ( - "github.com/spf13/cobra" -) +import "github.com/spf13/cobra" -var uninstallCmd = &cobra.Command{ - Use: "uninstall", - Short: "Uninstall radius for a specific platform", - Long: `Uninstall radius for a specific platform`, -} - -func init() { - RootCmd.AddCommand(uninstallCmd) +// NewCommand returns a new cobra command for `rad install`. +func NewCommand() *cobra.Command { + return &cobra.Command{ + Use: "install", + Short: "Installs Radius for a given platform", + Long: `Installs Radius for a given platform`, + } } diff --git a/pkg/cli/cmd/install/kubernetes/kubernetes.go b/pkg/cli/cmd/install/kubernetes/kubernetes.go new file mode 100644 index 00000000000..7faf49165ee --- /dev/null +++ b/pkg/cli/cmd/install/kubernetes/kubernetes.go @@ -0,0 +1,111 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubernetes + +import ( + "context" + + "github.com/project-radius/radius/pkg/cli/cmd/commonflags" + "github.com/project-radius/radius/pkg/cli/framework" + "github.com/project-radius/radius/pkg/cli/helm" + "github.com/project-radius/radius/pkg/cli/output" + "github.com/project-radius/radius/pkg/version" + "github.com/spf13/cobra" +) + +// NewCommand creates an instance of the `rad install kubernetes` command and runner. +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { + runner := NewRunner(factory) + + cmd := &cobra.Command{ + Use: "kubernetes", + Short: "Installs Radius onto a kubernetes cluster", + Long: "Installs Radius onto a kubernetes cluster.", + Example: `# install Radius with default settings on current kubernetes context +rad install kubernetes + +# install Radius and override the default chart values +rad install kubernetes --set key=value`, + Args: cobra.ExactArgs(0), + RunE: framework.RunCommand(runner), + } + + commonflags.AddKubeContextFlagVar(cmd, &runner.KubeContext) + cmd.Flags().BoolVar(&runner.Reinstall, "reinstall", false, "Specify to force reinstallation of Radius") + cmd.Flags().StringVar(&runner.Chart, "chart", "", "Specify a file path to a helm chart to install Radius from") + 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)") + + return cmd, runner +} + +// Runner is the Runner implementation for the `rad install kubernetes` command. +type Runner struct { + Helm helm.Interface + Output output.Interface + + KubeContext string + Chart string + Reinstall bool + Set []string +} + +// NewRunner creates an instance of the runner for the `rad install kubernetes` command. +func NewRunner(factory framework.Factory) *Runner { + return &Runner{ + Helm: factory.GetHelmInterface(), + Output: factory.GetOutput(), + } +} + +// Validate runs validation for the `rad install kubernetes` command. +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { + return nil +} + +// Run runs the `rad install kubernetes` command. +func (r *Runner) Run(ctx context.Context) error { + cliOptions := helm.CLIClusterOptions{ + Radius: helm.RadiusOptions{ + Reinstall: r.Reinstall, + ChartPath: r.Chart, + Set: r.Set, + }, + } + + state, err := r.Helm.CheckRadiusInstall(r.KubeContext) + if err != nil { + return err + } + + version := version.Version() + if state.Installed && r.Reinstall { + r.Output.LogInfo("Reinstalling Radius version %s to namespace: %s...", version, helm.RadiusSystemNamespace) + } else if state.Installed { + r.Output.LogInfo("Found existing Radius installation. Use '--reinstall' to force reinstallation.") + return nil + } else { + r.Output.LogInfo("Installing Radius version %s to namespace: %s...", version, helm.RadiusSystemNamespace) + } + + clusterOptions := helm.PopulateDefaultClusterOptions(cliOptions) + _, err = r.Helm.InstallRadius(ctx, clusterOptions, r.KubeContext) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/cli/cmd/install/kubernetes/kubernetes_test.go b/pkg/cli/cmd/install/kubernetes/kubernetes_test.go new file mode 100644 index 00000000000..06bf92851b9 --- /dev/null +++ b/pkg/cli/cmd/install/kubernetes/kubernetes_test.go @@ -0,0 +1,177 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubernetes + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/project-radius/radius/pkg/cli/helm" + "github.com/project-radius/radius/pkg/cli/output" + "github.com/project-radius/radius/test/radcli" + "github.com/stretchr/testify/require" +) + +func Test_CommandValidation(t *testing.T) { + radcli.SharedCommandValidation(t, NewCommand) +} + +func Test_Validate(t *testing.T) { + testcases := []radcli.ValidateInput{ + { + Name: "valid (basic)", + Input: []string{}, + ExpectedValid: true, + }, + { + Name: "valid (advanced)", + Input: []string{"--reinstall", "--kubecontext", "foo", "--set", "foo=bar", "--set", "bar=baz"}, + ExpectedValid: true, + }, + { + Name: "too many args", + Input: []string{"blah"}, + ExpectedValid: false, + }, + } + radcli.SharedValidateValidation(t, NewCommand, testcases) +} + +func Test_Run(t *testing.T) { + t.Run("Success: Install", func(t *testing.T) { + ctrl := gomock.NewController(t) + helmMock := helm.NewMockInterface(ctrl) + outputMock := &output.MockOutput{} + + ctx := context.Background() + runner := &Runner{ + Helm: helmMock, + Output: outputMock, + + KubeContext: "test-context", + Chart: "test-chart", + Set: []string{"foo=bar", "bar=baz"}, + } + + helmMock.EXPECT().CheckRadiusInstall("test-context"). + Return(helm.InstallState{}, nil). + Times(1) + + expectedOptions := helm.PopulateDefaultClusterOptions(helm.CLIClusterOptions{ + Radius: helm.RadiusOptions{ + ChartPath: "test-chart", + Set: []string{"foo=bar", "bar=baz"}, + }, + }) + helmMock.EXPECT().InstallRadius(ctx, expectedOptions, "test-context"). + Return(true, nil). + Times(1) + + err := runner.Run(ctx) + require.NoError(t, err) + + expectedWrites := []any{ + output.LogOutput{ + Format: "Installing Radius version %s to namespace: %s...", + Params: []interface{}{"edge", "radius-system"}, + }, + } + require.Equal(t, expectedWrites, outputMock.Writes) + }) + t.Run("Success: Already Installed", func(t *testing.T) { + ctrl := gomock.NewController(t) + helmMock := helm.NewMockInterface(ctrl) + outputMock := &output.MockOutput{} + + ctx := context.Background() + runner := &Runner{ + Helm: helmMock, + Output: outputMock, + + KubeContext: "test-context", + Chart: "test-chart", + Set: []string{"foo=bar", "bar=baz"}, + } + + helmMock.EXPECT().CheckRadiusInstall("test-context"). + Return(helm.InstallState{Installed: true, Version: "test-version"}, nil). + Times(1) + + expectedOptions := helm.PopulateDefaultClusterOptions(helm.CLIClusterOptions{ + Radius: helm.RadiusOptions{ + ChartPath: "test-chart", + Set: []string{"foo=bar", "bar=baz"}, + }, + }) + helmMock.EXPECT().InstallRadius(ctx, expectedOptions, "test-context"). + Return(true, nil). + Times(1) + + err := runner.Run(ctx) + require.NoError(t, err) + + expectedWrites := []any{ + output.LogOutput{ + Format: "Found existing Radius installation. Use '--reinstall' to force reinstallation.", + }, + } + require.Equal(t, expectedWrites, outputMock.Writes) + }) + t.Run("Success: Reinstall", func(t *testing.T) { + ctrl := gomock.NewController(t) + helmMock := helm.NewMockInterface(ctrl) + outputMock := &output.MockOutput{} + + ctx := context.Background() + runner := &Runner{ + Helm: helmMock, + Output: outputMock, + + KubeContext: "test-context", + Chart: "test-chart", + Set: []string{"foo=bar", "bar=baz"}, + Reinstall: true, + } + + helmMock.EXPECT().CheckRadiusInstall("test-context"). + Return(helm.InstallState{Installed: true, Version: "test-version"}, nil). + Times(1) + + expectedOptions := helm.PopulateDefaultClusterOptions(helm.CLIClusterOptions{ + Radius: helm.RadiusOptions{ + ChartPath: "test-chart", + Set: []string{"foo=bar", "bar=baz"}, + Reinstall: true, + }, + }) + helmMock.EXPECT().InstallRadius(ctx, expectedOptions, "test-context"). + Return(true, nil). + Times(1) + + err := runner.Run(ctx) + require.NoError(t, err) + + expectedWrites := []any{ + output.LogOutput{ + Format: "Reinstalling Radius version %s to namespace: %s...", + Params: []interface{}{"edge", "radius-system"}, + }, + } + require.Equal(t, expectedWrites, outputMock.Writes) + }) +} diff --git a/pkg/cli/cmd/uninstall/kubernetes/kubernetes.go b/pkg/cli/cmd/uninstall/kubernetes/kubernetes.go new file mode 100644 index 00000000000..d9dfe6a9858 --- /dev/null +++ b/pkg/cli/cmd/uninstall/kubernetes/kubernetes.go @@ -0,0 +1,90 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubernetes + +import ( + "context" + + "github.com/project-radius/radius/pkg/cli/cmd/commonflags" + "github.com/project-radius/radius/pkg/cli/framework" + "github.com/project-radius/radius/pkg/cli/helm" + "github.com/project-radius/radius/pkg/cli/output" + "github.com/spf13/cobra" +) + +// NewCommand creates an instance of the `rad ` command and runner. +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { + runner := NewRunner(factory) + + cmd := &cobra.Command{ + Use: "kubernetes", + Short: "Uninstall Radius from a Kubernetes cluster", + Long: `Uninstall Radius from a Kubernetes cluster.`, + Example: `# uninstall Radius from the current Kubernetes cluster +rad uninstall kubernetes + +# uninstall Radius from a specific Kubernetes cluster based on the Kubeconfig context +rad uninstall kubernetes --kubecontext my-kubecontext`, + Args: cobra.ExactArgs(0), + RunE: framework.RunCommand(runner), + } + + commonflags.AddKubeContextFlagVar(cmd, &runner.KubeContext) + + return cmd, runner +} + +// Runner is the Runner implementation for the `rad uninstall kubernetes` command. +type Runner struct { + Helm helm.Interface + Output output.Interface + + KubeContext string +} + +// NewRunner creates an instance of the runner for the `rad uninstall kubernetes` command. +func NewRunner(factory framework.Factory) *Runner { + return &Runner{ + Output: factory.GetOutput(), + } +} + +// Validate runs validation for the `rad uninstall kubernetes` command. +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { + return nil +} + +// Run runs the `rad uninstall kubernetes` command. +func (r *Runner) Run(ctx context.Context) error { + state, err := r.Helm.CheckRadiusInstall(r.KubeContext) + if err != nil { + return err + } + if !state.Installed { + r.Output.LogInfo("Radius is not installed on the Kubernetes cluster") + return nil + } + + r.Output.LogInfo("Uninstalling Radius...") + err = r.Helm.UninstallRadius(ctx, r.KubeContext) + if err != nil { + return err + } + + r.Output.LogInfo("Radius was uninstalled successfully. Any existing environment metadata will be retained for future installations. Local workspaces are also retained. Use the rad workspace command if updates are needed to your local workspaces.") + return nil +} diff --git a/pkg/cli/cmd/uninstall/kubernetes/kubernetes_test.go b/pkg/cli/cmd/uninstall/kubernetes/kubernetes_test.go new file mode 100644 index 00000000000..0791ed2bdf0 --- /dev/null +++ b/pkg/cli/cmd/uninstall/kubernetes/kubernetes_test.go @@ -0,0 +1,118 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubernetes + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/project-radius/radius/pkg/cli/helm" + "github.com/project-radius/radius/pkg/cli/output" + "github.com/project-radius/radius/test/radcli" + "github.com/stretchr/testify/require" +) + +func Test_CommandValidation(t *testing.T) { + radcli.SharedCommandValidation(t, NewCommand) +} + +func Test_Validate(t *testing.T) { + testcases := []radcli.ValidateInput{ + { + Name: "valid", + Input: []string{}, + ExpectedValid: true, + }, + { + Name: "valid (advanced)", + Input: []string{"--kubecontext", "test-context"}, + ExpectedValid: true, + }, + { + Name: "too many args", + Input: []string{"blah"}, + ExpectedValid: false, + }, + } + radcli.SharedValidateValidation(t, NewCommand, testcases) +} + +func Test_Run(t *testing.T) { + t.Run("Success: Installed -> Uninstalled", func(t *testing.T) { + ctrl := gomock.NewController(t) + helmMock := helm.NewMockInterface(ctrl) + outputMock := &output.MockOutput{} + + ctx := context.Background() + runner := &Runner{ + Helm: helmMock, + Output: outputMock, + + KubeContext: "test-context", + } + + helmMock.EXPECT().CheckRadiusInstall("test-context"). + Return(helm.InstallState{Installed: true, Version: "test-version"}, nil). + Times(1) + + helmMock.EXPECT().UninstallRadius(ctx, "test-context"). + Return(nil). + Times(1) + + err := runner.Run(ctx) + require.NoError(t, err) + + expectedWrites := []any{ + output.LogOutput{ + Format: "Uninstalling Radius...", + }, + output.LogOutput{ + Format: "Radius was uninstalled successfully. Any existing environment metadata will be retained for future installations. Local workspaces are also retained. Use the rad workspace command if updates are needed to your local workspaces.", + }, + } + require.Equal(t, expectedWrites, outputMock.Writes) + }) + + t.Run("Success: Not Installed -> Uninstalled", func(t *testing.T) { + ctrl := gomock.NewController(t) + helmMock := helm.NewMockInterface(ctrl) + outputMock := &output.MockOutput{} + + ctx := context.Background() + runner := &Runner{ + Helm: helmMock, + Output: outputMock, + + KubeContext: "test-context", + } + + helmMock.EXPECT().CheckRadiusInstall("test-context"). + Return(helm.InstallState{}, nil). + Times(1) + + err := runner.Run(ctx) + require.NoError(t, err) + + expectedWrites := []any{ + output.LogOutput{ + Format: "Radius is not installed on the Kubernetes cluster", + }, + } + require.Equal(t, expectedWrites, outputMock.Writes) + }) +} diff --git a/pkg/cli/cmd/uninstall/uninstall.go b/pkg/cli/cmd/uninstall/uninstall.go new file mode 100644 index 00000000000..86fa7b91e43 --- /dev/null +++ b/pkg/cli/cmd/uninstall/uninstall.go @@ -0,0 +1,28 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package uninstall + +import "github.com/spf13/cobra" + +// NewCommand returns a new cobra command for `rad uninstall`. +func NewCommand() *cobra.Command { + return &cobra.Command{ + Use: "uninstall", + Short: "Uninstall Radius for a specific platform", + Long: `Uninstall Radius for a specific platform`, + } +} diff --git a/pkg/cli/helm/cluster.go b/pkg/cli/helm/cluster.go index 44e1ccee65e..3f3200cb462 100644 --- a/pkg/cli/helm/cluster.go +++ b/pkg/cli/helm/cluster.go @@ -50,18 +50,12 @@ func NewDefaultClusterOptions() ClusterOptions { chartVersion = fmt.Sprintf("~%s", version.ChartVersion()) } - tag := version.Channel() - if version.IsEdgeChannel() { - tag = "latest" - } - return ClusterOptions{ Contour: ContourOptions{ ChartVersion: ContourChartDefaultVersion, }, Radius: RadiusOptions{ ChartVersion: chartVersion, - ImageVersion: tag, }, } } @@ -78,8 +72,8 @@ func PopulateDefaultClusterOptions(cliOptions CLIClusterOptions) ClusterOptions options.Radius.ChartPath = cliOptions.Radius.ChartPath } - if len(cliOptions.Radius.Values) > 0 { - options.Radius.Values = cliOptions.Radius.Values + if len(cliOptions.Radius.Set) > 0 { + options.Radius.Set = cliOptions.Radius.Set } return options } @@ -188,6 +182,9 @@ type Interface interface { // InstallRadius installs Radius on the cluster, based on the specified Kubernetes context. InstallRadius(ctx context.Context, clusterOptions ClusterOptions, kubeContext string) (bool, error) + + // UninstallRadius uninstalls Radius from the cluster based on the specified Kubernetes context. Will succeed regardless of whether Radius is installed. + UninstallRadius(ctx context.Context, kubeContext string) error } type Impl struct { @@ -202,3 +199,7 @@ func (i *Impl) CheckRadiusInstall(kubeContext string) (InstallState, error) { func (i *Impl) InstallRadius(ctx context.Context, clusterOptions ClusterOptions, kubeContext string) (bool, error) { return Install(ctx, clusterOptions, kubeContext) } + +func (i *Impl) UninstallRadius(ctx context.Context, kubeContext string) error { + return UninstallOnCluster(kubeContext) +} diff --git a/pkg/cli/helm/mock_cluster.go b/pkg/cli/helm/mock_cluster.go index 243298e3b97..3d6eee30d7a 100644 --- a/pkg/cli/helm/mock_cluster.go +++ b/pkg/cli/helm/mock_cluster.go @@ -63,3 +63,17 @@ func (mr *MockInterfaceMockRecorder) InstallRadius(arg0, arg1, arg2 interface{}) mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallRadius", reflect.TypeOf((*MockInterface)(nil).InstallRadius), arg0, arg1, arg2) } + +// UninstallRadius mocks base method. +func (m *MockInterface) UninstallRadius(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UninstallRadius", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UninstallRadius indicates an expected call of UninstallRadius. +func (mr *MockInterfaceMockRecorder) UninstallRadius(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallRadius", reflect.TypeOf((*MockInterface)(nil).UninstallRadius), arg0, arg1) +} diff --git a/pkg/cli/helm/radiusclient.go b/pkg/cli/helm/radiusclient.go index 9957a6fc624..9503f818e7b 100644 --- a/pkg/cli/helm/radiusclient.go +++ b/pkg/cli/helm/radiusclient.go @@ -22,15 +22,13 @@ import ( "fmt" "strings" + "github.com/project-radius/radius/pkg/cli/output" helm "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/storage/driver" "helm.sh/helm/v3/pkg/strvals" "k8s.io/cli-runtime/pkg/genericclioptions" - - "github.com/project-radius/radius/pkg/cli/output" - "github.com/project-radius/radius/pkg/version" ) const ( @@ -43,8 +41,7 @@ type RadiusOptions struct { Reinstall bool ChartPath string ChartVersion string - ImageVersion string - Values string + Set []string } // Apply the radius helm chart. @@ -87,52 +84,39 @@ func ApplyRadiusHelmChart(options RadiusOptions, kubeContext string) (bool, erro histClient := helm.NewHistory(helmConf) histClient.Max = 1 // Only need to check if at least 1 exists - version := version.Version() // See: https://github.com/helm/helm/blob/281380f31ccb8eb0c86c84daf8bcbbd2f82dc820/cmd/helm/upgrade.go#L99 // The upgrade client's install option doesn't seem to work, so we have to check the history of releases manually // and invoke the install client. _, err = histClient.Run(radiusReleaseName) if errors.Is(err, driver.ErrReleaseNotFound) { - output.LogInfo("Installing Radius version %s control plane to namespace: %s", version, RadiusSystemNamespace) - err = runRadiusHelmInstall(helmConf, helmChart) if err != nil { return false, fmt.Errorf("failed to run Radius Helm install, err: \n%w\nHelm output:\n%s", err, helmOutput.String()) } } else if options.Reinstall { - output.LogInfo("Reinstalling Radius version %s control plane to namespace: %s", version, RadiusSystemNamespace) - err = runRadiusHelmUpgrade(helmConf, radiusReleaseName, helmChart) if err != nil { return false, fmt.Errorf("failed to run Radius Helm upgrade, err: \n%w\nHelm output:\n%s", err, helmOutput.String()) } } else if err == nil { alreadyInstalled = true - output.LogInfo("Found existing Radius installation. Use '--reinstall' to force reinstallation.") } return alreadyInstalled, err } // AddRadiusValues adds values to the helm chart. It overrides the default values in following order: // 1. lowest priority: Values from the helm chart default values.yaml -// 2. next priority: set correct image tag, potentially overwriting "latest" tag from step 1 -// 3. highest priority: Values by the --set flag potentially overwriting values from step 1 and 2 +// 2. highest priority: Values by the --set flag potentially overwriting values from step 1 and 2 func AddRadiusValues(helmChart *chart.Chart, options *RadiusOptions) error { values := helmChart.Values - services := []string{"rp", "ucp", "de"} - for _, service := range services { - if _, ok := values[service]; !ok { - values[service] = map[string]any{} + // Parse --set arguments in order so that the last one wins. + for _, arg := range options.Set { + err := strvals.ParseInto(arg, values) + if err != nil { + return err } - o := values[service].(map[string]any) - o["tag"] = options.ImageVersion - } - - err := strvals.ParseInto(options.Values, values) - if err != nil { - return err } return nil diff --git a/pkg/cli/helm/radiusclient_test.go b/pkg/cli/helm/radiusclient_test.go index 15d62a72c79..ef07e45ebb2 100644 --- a/pkg/cli/helm/radiusclient_test.go +++ b/pkg/cli/helm/radiusclient_test.go @@ -28,21 +28,13 @@ func Test_AddRadiusValues(t *testing.T) { var helmChart chart.Chart helmChart.Values = map[string]any{} options := &RadiusOptions{ - ImageVersion: "imageversion", - Values: "global.zipkin.url=url,global.prometheus.path=path", + Set: []string{"global.zipkin.url=url,global.prometheus.path=path"}, } err := AddRadiusValues(&helmChart, options) require.Equal(t, err, nil) values := helmChart.Values - // validate tags for ucp, de, and rp - for _, k := range []string{"ucp", "de", "rp"} { - o := values[k].(map[string]any) - _, ok := o["tag"] - assert.True(t, ok) - assert.Equal(t, o["tag"], "imageversion") - } _, ok := values["global"] assert.True(t, ok) @@ -66,21 +58,13 @@ func Test_AddRadiusValuesOverrideWithSet(t *testing.T) { var helmChart chart.Chart helmChart.Values = map[string]any{} options := &RadiusOptions{ - ImageVersion: "imageversion", - Values: "rp.image=radius.azurecr.io/appcore-rp,rp.tag=latest,global.zipkin.url=url,global.prometheus.path=path", + Set: []string{"rp.image=radius.azurecr.io/appcore-rp,rp.tag=latest", "global.zipkin.url=url,global.prometheus.path=path"}, } err := AddRadiusValues(&helmChart, options) require.Equal(t, err, nil) values := helmChart.Values - // validate tags for ucp, de - for _, k := range []string{"ucp", "de"} { - o := values[k].(map[string]any) - _, ok := o["tag"] - assert.True(t, ok) - assert.Equal(t, o["tag"], "imageversion") - } // validate image, tag for rp should have been overridden with latest o := values["rp"].(map[string]any) diff --git a/pkg/cli/setup/chart.go b/pkg/cli/setup/chart.go deleted file mode 100644 index 40a6b1d9736..00000000000 --- a/pkg/cli/setup/chart.go +++ /dev/null @@ -1,105 +0,0 @@ -/* -Copyright 2023 The Radius Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package setup - -import ( - "fmt" - "strings" - - "github.com/spf13/cobra" -) - -type ChartArgs struct { - Reinstall bool - ChartPath string - AppCoreImage string - AppCoreTag string - UcpImage string - UcpTag string - - // PublicEndpointOverride is used to define the public endpoint of the Kubernetes cluster - // for display purposes. This is useful when the actual public IP address of a cluster's ingress - // is not a routable IP. This comes up all of the time for a local cluster. - PublicEndpointOverride string - - // Values is a string consisting of list of values to pass to the Helm chart which would be used to override values in the chart. - Values string -} - -// RegisterPersistentChartArgs registers the CLI arguments used for our Helm chart. -func RegisterPersistentChartArgs(cmd *cobra.Command) { - cmd.PersistentFlags().Bool("reinstall", false, "Specify to force reinstallation of Radius") - cmd.PersistentFlags().String("chart", "", "Specify a file path to a helm chart to install Radius from") - cmd.PersistentFlags().String("image", "", "Specify the radius controller image to use") - cmd.PersistentFlags().String("tag", "", "Specify the radius controller tag to use") - cmd.PersistentFlags().String("appcore-image", "", "Specify Application.Core RP image to use") - cmd.PersistentFlags().String("appcore-tag", "", "Specify Application.Core RP image tag to use") - cmd.PersistentFlags().String("ucp-image", "", "Specify the UCP image to use") - cmd.PersistentFlags().String("ucp-tag", "", "Specify the UCP tag to use") - cmd.PersistentFlags().String("public-endpoint-override", "", "Specify the public IP address or hostname of the Kubernetes cluster. It must be in the format: [:]. Ex: 'localhost:9000'") - cmd.PersistentFlags().String("set", "", "Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") -} - -// ParseChartArgs the arguments we provide for installation of the Helm chart. -func ParseChartArgs(cmd *cobra.Command) (*ChartArgs, error) { - reinstall, err := cmd.Flags().GetBool("reinstall") - if err != nil { - return nil, err - } - chartPath, err := cmd.Flags().GetString("chart") - if err != nil { - return nil, err - } - appcoreImage, err := cmd.Flags().GetString("appcore-image") - if err != nil { - return nil, err - } - appcoreTag, err := cmd.Flags().GetString("appcore-tag") - if err != nil { - return nil, err - } - ucpImage, err := cmd.Flags().GetString("ucp-image") - if err != nil { - return nil, err - } - ucpTag, err := cmd.Flags().GetString("ucp-tag") - if err != nil { - return nil, err - } - values, err := cmd.Flags().GetString("set") - if err != nil { - return nil, err - } - - publicEndpointOverride, err := cmd.Flags().GetString("public-endpoint-override") - if err != nil { - return nil, err - } else if strings.HasPrefix(publicEndpointOverride, "http://") || strings.HasPrefix(publicEndpointOverride, "https://") { - return nil, fmt.Errorf("a URL is not accepted here. Please specify the public endpoint override in the form [:]. Ex: 'localhost:9000'") - } - - return &ChartArgs{ - Reinstall: reinstall, - ChartPath: chartPath, - AppCoreImage: appcoreImage, - AppCoreTag: appcoreTag, - UcpImage: ucpImage, - UcpTag: ucpTag, - PublicEndpointOverride: publicEndpointOverride, - Values: values, - }, nil -} diff --git a/pkg/cli/setup/resourcegroups.go b/pkg/cli/setup/resourcegroups.go deleted file mode 100644 index cca3e820398..00000000000 --- a/pkg/cli/setup/resourcegroups.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2023 The Radius Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package setup - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/project-radius/radius/pkg/sdk" - "github.com/project-radius/radius/pkg/ucp/api/v20220901privatepreview" -) - -type ErrUCPResourceGroupCreationFailed struct { - resp *http.Response - err error -} - -func (e *ErrUCPResourceGroupCreationFailed) Error() string { - if e.resp == nil { - return fmt.Sprintf("failed to create UCP resourceGroup: %s", e.err) - } - - return fmt.Sprintf("request to create UCP resourceGroup failed with status: %d, response: %+v", e.resp.StatusCode, e.resp) -} - -func (e *ErrUCPResourceGroupCreationFailed) Is(target error) bool { - _, ok := target.(*ErrUCPResourceGroupCreationFailed) - return ok -} - -// TODO remove this when envInit is removed. DO NOT add new uses of this function. Use the generated client. -func CreateWorkspaceResourceGroup(ctx context.Context, connection sdk.Connection, name string) (string, error) { - id, err := createUCPResourceGroup(ctx, connection, name, "/planes/radius/local") - if err != nil { - return "", err - } - - // TODO: we TEMPORARILY create a resource group in the deployments plane because the deployments RP requires it. - // We'll remove this in the future. - _, err = createUCPResourceGroup(ctx, connection, name, "/planes/deployments/local") - if err != nil { - return "", err - } - - return id, nil -} - -func createUCPResourceGroup(ctx context.Context, connection sdk.Connection, resourceGroupName string, plane string) (string, error) { - createRgRequest, err := http.NewRequest( - http.MethodPut, - fmt.Sprintf("%s%s/resourceGroups/%s?api-version=%s", connection.Endpoint(), plane, resourceGroupName, v20220901privatepreview.Version), - strings.NewReader(`{ - "location": "global" - }`)) - - if err != nil { - return "", &ErrUCPResourceGroupCreationFailed{nil, err} - } - createRgRequest = createRgRequest.WithContext(ctx) - createRgRequest.Header.Add("Content-Type", "application/json") - - resp, err := connection.Client().Do(createRgRequest) - if err != nil || (resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK) { - return "", &ErrUCPResourceGroupCreationFailed{resp, err} - } - - defer resp.Body.Close() - var jsonBody map[string]any - if json.NewDecoder(resp.Body).Decode(&jsonBody) != nil { - return "", nil - } - - return jsonBody["id"].(string), nil -} diff --git a/pkg/cli/setup/uninstall.go b/pkg/cli/setup/uninstall.go deleted file mode 100644 index b2fafe6c5c2..00000000000 --- a/pkg/cli/setup/uninstall.go +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2023 The Radius Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package setup - -import ( - "context" - - "github.com/project-radius/radius/pkg/cli/helm" - "github.com/project-radius/radius/pkg/cli/output" -) - -func Uninstall(ctx context.Context, kubeContext string) error { - step := output.BeginStep("Uninstalling Radius...") - err := helm.UninstallOnCluster(kubeContext) - if err != nil { - return err - } - output.LogInfo("The Radius control plane was uninstalled successfully. Any existing environment metadata will be retained for future installations. Local workspaces are also retained. Use the rad workspace command if updates are needed to your local workspaces.") - output.CompleteStep(step) - return nil -}