Skip to content

Commit

Permalink
Add purge flag to delete radius related namespace (radius-project#7847)
Browse files Browse the repository at this point in the history
# Description

Add purge flag to delete radius related namespace, the purge flag should
be set explicitly(--purge=true).

## Type of change

- This pull request adds or changes features of Radius and has an
approved issue (issue link required).

issue link radius-project#7764

---------

Signed-off-by: lbzs <627062293@qq.com>
Co-authored-by: Ryan Nowak <nowakra@gmail.com>
  • Loading branch information
lbzss and rynowak authored Sep 5, 2024
1 parent 5366a82 commit 9091f8b
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 4 deletions.
21 changes: 17 additions & 4 deletions pkg/cli/cmd/uninstall/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package kubernetes

import (
"context"
"github.com/radius-project/radius/pkg/cli/kubernetes"

"github.com/radius-project/radius/pkg/cli/cmd/commonflags"
"github.com/radius-project/radius/pkg/cli/framework"
Expand Down Expand Up @@ -48,23 +49,27 @@ rad uninstall kubernetes --kubecontext my-kubecontext`,
}

commonflags.AddKubeContextFlagVar(cmd, &runner.KubeContext)
cmd.Flags().BoolVar(&runner.Purge, "purge", false, "Delete all data stored by Radius.")

return cmd, runner
}

// Runner is the Runner implementation for the `rad uninstall kubernetes` command.
type Runner struct {
Helm helm.Interface
Output output.Interface
Helm helm.Interface
Output output.Interface
Kubernetes kubernetes.Interface

KubeContext string
Purge bool
}

// NewRunner creates an instance of the runner for the `rad uninstall kubernetes` command.
func NewRunner(factory framework.Factory) *Runner {
return &Runner{
Helm: factory.GetHelmInterface(),
Output: factory.GetOutput(),
Helm: factory.GetHelmInterface(),
Output: factory.GetOutput(),
Kubernetes: factory.GetKubernetesInterface(),
}
}

Expand Down Expand Up @@ -97,6 +102,14 @@ func (r *Runner) Run(ctx context.Context) error {
return err
}

if r.Purge {
r.Output.LogInfo("Deleting namespace %s", helm.RadiusSystemNamespace)
if err := r.Kubernetes.DeleteNamespace(r.KubeContext); err != nil {
return err
}
r.Output.LogInfo("Radius was fully uninstalled. Any existing data have been removed.")
return nil
}
r.Output.LogInfo("Radius was uninstalled successfully. Any existing data will be retained for future installations. Local configuration is also retained. Use the `rad workspace` command if updates are needed to your configuration.")
return nil
}
44 changes: 44 additions & 0 deletions pkg/cli/cmd/uninstall/kubernetes/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package kubernetes

import (
"context"
"github.com/radius-project/radius/pkg/cli/kubernetes"
"testing"

"github.com/radius-project/radius/pkg/cli/helm"
Expand Down Expand Up @@ -115,4 +116,47 @@ func Test_Run(t *testing.T) {
}
require.Equal(t, expectedWrites, outputMock.Writes)
})
t.Run("Success: Installed -> Uninstalled -> Purge)", func(t *testing.T) {
ctrl := gomock.NewController(t)
helmMock := helm.NewMockInterface(ctrl)
outputMock := &output.MockOutput{}
k8sMock := kubernetes.NewMockInterface(ctrl)

ctx := context.Background()
runner := &Runner{
Helm: helmMock,
Output: outputMock,
Kubernetes: k8sMock,

KubeContext: "test-context",
Purge: true,
}

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)

k8sMock.EXPECT().DeleteNamespace("test-context").Return(nil).Times(1)

err := runner.Run(ctx)
require.NoError(t, err)

expectedWrites := []any{
output.LogOutput{
Format: "Uninstalling Radius...",
},
output.LogOutput{
Format: "Deleting namespace %s",
Params: []any{helm.RadiusSystemNamespace},
},
output.LogOutput{
Format: "Radius was fully uninstalled. Any existing data have been removed.",
},
}
require.Equal(t, expectedWrites, outputMock.Writes)
})
}
40 changes: 40 additions & 0 deletions pkg/cli/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ import (
"context"
"errors"
"fmt"
"github.com/radius-project/radius/pkg/cli/helm"
"k8s.io/apimachinery/pkg/util/wait"
"time"

contourv1 "github.com/projectcontour/contour/apis/projectcontour/v1"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8s_runtime "k8s.io/apimachinery/pkg/runtime"
applycorev1 "k8s.io/client-go/applyconfigurations/core/v1"
Expand Down Expand Up @@ -117,6 +121,30 @@ func EnsureNamespace(ctx context.Context, client k8s.Interface, namespace string
return nil
}

// deleteNamespace delete the specified namespace.
func deleteNamespace(ctx context.Context, client k8s.Interface, namespace string) error {
if err := client.CoreV1().Namespaces().
Delete(ctx, namespace, metav1.DeleteOptions{}); err != nil {
return err
}

// Ensure the namespace is deleted. This will block until the namespace is no longer exist.
err := wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (done bool, err error) {
_, err = client.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return true, nil
}
return false, err
}
return false, nil
})
if err != nil {
return err
}
return nil
}

// NewCLIClientConfig creates new Kubernetes client config loading from local home directory with CLI options.
//

Expand Down Expand Up @@ -160,6 +188,7 @@ func GetContextFromConfigFileIfExists(configFilePath, context string) (string, e
//go:generate mockgen -typed -destination=./mock_kubernetes.go -package=kubernetes -self_package github.com/radius-project/radius/pkg/cli/kubernetes github.com/radius-project/radius/pkg/cli/kubernetes Interface
type Interface interface {
GetKubeContext() (*api.Config, error)
DeleteNamespace(string) error
}

type Impl struct {
Expand All @@ -172,3 +201,14 @@ type Impl struct {
func (i *Impl) GetKubeContext() (*api.Config, error) {
return kubeutil.LoadConfigFile("")
}

func (i *Impl) DeleteNamespace(kubeContext string) error {
clientSet, _, err := NewClientset(kubeContext)
if err != nil {
return err
}
if err := deleteNamespace(context.Background(), clientSet, helm.RadiusSystemNamespace); err != nil {
return err
}
return nil
}
14 changes: 14 additions & 0 deletions pkg/cli/kubernetes/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/radius-project/radius/test/k8sutil"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
k8sfake "k8s.io/client-go/kubernetes/fake"
Expand All @@ -49,6 +50,19 @@ func TestEnsureNamespace(t *testing.T) {
require.NoError(t, err)
}

func TestDeleteNamespace(t *testing.T) {
namespace := "radius-test"
f := k8sfake.NewSimpleClientset(&v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: namespace}})

ctx := context.Background()
_, err := f.CoreV1().Namespaces().Get(ctx, namespace, meta_v1.GetOptions{})
require.NoError(t, err)
err = deleteNamespace(ctx, f, namespace)
require.NoError(t, err)
_, err = f.CoreV1().Namespaces().Get(ctx, namespace, meta_v1.GetOptions{})
require.True(t, apierrors.IsNotFound(err), "expected not found error but got %v", err)
}

func TestGetContextFromConfigFileIfExists(t *testing.T) {
configFile, err := os.CreateTemp("", "")
require.NoError(t, err)
Expand Down
38 changes: 38 additions & 0 deletions pkg/cli/kubernetes/mock_kubernetes.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9091f8b

Please sign in to comment.