diff --git a/cli/cmd/namespace.go b/cli/cmd/namespace.go index 09fb2f72..c5e577bd 100644 --- a/cli/cmd/namespace.go +++ b/cli/cmd/namespace.go @@ -13,6 +13,8 @@ func NewNamespaceCommand() *cobra.Command { Long: `Configure the namespace settings for Drasi`, } namespaceCommand.AddCommand(setNamespaceCommand()) + namespaceCommand.AddCommand(getNamespaceCommand()) + namespaceCommand.AddCommand(listNamespaceCommand()) return namespaceCommand } @@ -25,7 +27,7 @@ This commands assumes that Drasi has been installed to the namespace specified.` Args: cobra.MinimumNArgs(0), RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 1 { - return fmt.Errorf("Too many arguments") + return fmt.Errorf("too many arguments") } var err error var namespace string @@ -45,7 +47,7 @@ This commands assumes that Drasi has been installed to the namespace specified.` clusterConfig.DrasiNamespace = namespace saveConfig(clusterConfig) } else { - return fmt.Errorf("Namespace cannot be empty") + return fmt.Errorf("namespace cannot be empty") } cfg := readConfig() @@ -56,3 +58,38 @@ This commands assumes that Drasi has been installed to the namespace specified.` } return setNamespaceCommand } + +func getNamespaceCommand() *cobra.Command { + return &cobra.Command{ + Use: "get", + Short: "Get the current namespace", + Long: `Retrieve the current Drasi namespace`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg := readConfig() + fmt.Println("Current namespace: " + cfg.DrasiNamespace) + return nil + }, + } +} + +func listNamespaceCommand() *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List all namespaces", + Long: `List all namespaces that have Drasi installed.`, + RunE: func(cmd *cobra.Command, args []string) error { + // Logic to list all namespaces + namespaces, err := listNamespaces() + if err != nil { + return err + } + + fmt.Println("Namespaces:") + for _, ns := range namespaces { + fmt.Println(ns) + } + + return nil + }, + } +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 2fb0e8c1..afe62227 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -15,6 +15,7 @@ func init() { NewListCommand(), NewWaitCommand(), NewNamespaceCommand(), + NewUninstallCommand(), ) RootCommand.PersistentFlags().StringP("namespace", "n", "drasi-system", "Kubernetes namespace to install Drasi into") diff --git a/cli/cmd/uninstall.go b/cli/cmd/uninstall.go new file mode 100644 index 00000000..fcdb18e8 --- /dev/null +++ b/cli/cmd/uninstall.go @@ -0,0 +1,84 @@ +package cmd + +import ( + "fmt" + "log" + "strings" + + "drasi.io/cli/service" + "github.com/spf13/cobra" +) + +func NewUninstallCommand() *cobra.Command { + var uninstallCommand = &cobra.Command{ + Use: "uninstall ", + Short: "Uninstall Drasi", + Long: `Uninstall Drasi from the current namespace`, + Args: cobra.MinimumNArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + cfg := readConfig() + var err error + var currentNamespace string + if currentNamespace, err = cmd.Flags().GetString("namespace"); err != nil { + return err + } + if !cmd.Flags().Changed("namespace") { + currentNamespace = cfg.DrasiNamespace + } + + fmt.Println("Uninstalling Drasi") + fmt.Println("Deleting namespace: ", currentNamespace) + + // Ask for confirmation if the user didn't pass the -y flag + if !cmd.Flags().Changed("yes") { + fmt.Printf("Are you sure you want to uninstall Drasi from the namespace %s? (yes/no): ", currentNamespace) + if !askForConfirmation(currentNamespace) { + fmt.Println("Uninstall cancelled") + return nil + } + } + + err = service.UninstallDrasi(currentNamespace) + if err != nil { + return err + } + + fmt.Println("Drasi uninstalled successfully") + + if uninstallDapr, _ := cmd.Flags().GetBool("uninstall-dapr"); uninstallDapr { + fmt.Println("Uninstalling Dapr") + err = service.UninstallDapr(currentNamespace) + if err != nil { + return err + } + fmt.Println("Dapr uninstalled successfully") + } + + return nil + }, + } + + uninstallCommand.Flags().BoolP("yes", "y", false, "Automatic yes to prompts") + uninstallCommand.Flags().BoolP("uninstall-dapr", "d", false, "Uninstall Dapr by deleting the Dapr system namespace") + + return uninstallCommand +} + +func askForConfirmation(namespace string) bool { + var response string + + _, err := fmt.Scanln(&response) + if err != nil { + log.Fatal(err) + } + + switch strings.ToLower(response) { + case "y", "yes": + return true + case "n", "no": + return false + default: + fmt.Println("I'm sorry but I didn't get what you meant, please type (y)es or (n)o and then press enter:") + return askForConfirmation(namespace) + } +} diff --git a/cli/cmd/utils.go b/cli/cmd/utils.go index b7ac8d83..5ce41edc 100644 --- a/cli/cmd/utils.go +++ b/cli/cmd/utils.go @@ -2,6 +2,7 @@ package cmd import ( "bufio" + "context" "encoding/json" "io" "os/user" @@ -12,6 +13,9 @@ import ( "drasi.io/cli/api" "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" ) type ClusterConfig struct { @@ -109,3 +113,37 @@ func readConfig() ClusterConfig { json.Unmarshal(data, &cfg) return cfg } + +// Retrieve the name of all namespaces that have the label +// "drasi.io/namespace": "true" +func listNamespaces() ([]string, error) { + configLoadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + configOverrides := &clientcmd.ConfigOverrides{} + + config := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(configLoadingRules, configOverrides) + + restConfig, err := config.ClientConfig() + + if err != nil { + return nil, err + } + + clientset, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return nil, err + } + + namespaces, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{ + LabelSelector: "drasi.io/namespace=true", + }) + if err != nil { + return nil, err + } + + var nsList []string + for _, ns := range namespaces.Items { + nsList = append(nsList, ns.Name) + } + + return nsList, nil +} diff --git a/cli/service/installer.go b/cli/service/installer.go index 71a1c180..95246bc3 100644 --- a/cli/service/installer.go +++ b/cli/service/installer.go @@ -596,6 +596,9 @@ func CreateNamespace(config *rest.Config, namespace string) error { newNamespace := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: namespace, + Labels: map[string]string{ + "drasi.io/namespace": "true", + }, }, } diff --git a/cli/service/uninstaller.go b/cli/service/uninstaller.go new file mode 100644 index 00000000..d58ecf1b --- /dev/null +++ b/cli/service/uninstaller.go @@ -0,0 +1,99 @@ +package service + +import ( + "context" + "fmt" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +func UninstallDrasi(namespace string) error { + configLoadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + configOverrides := &clientcmd.ConfigOverrides{} + + config := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(configLoadingRules, configOverrides) + + restConfig, err := config.ClientConfig() + + if err != nil { + return err + } + + clientset, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return err + } + + err = clientset.CoreV1().Namespaces().Delete(context.TODO(), namespace, metav1.DeleteOptions{}) + if err != nil { + return err + } + + // Pods are not deleted immediately; instead, they will remain in a Terminating state for a while + // Need to verify that all resources have been deleted; if not, wait for them to be deleted + nsDeleted := false + for !nsDeleted { + list, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return err + } + for _, ns := range list.Items { + // check if the namespace is still there + if ns.Name == namespace { + fmt.Println("Namespace is still present. Waiting for it to be deleted") + // wait for 10 seconds + time.Sleep(10 * time.Second) + } else { + nsDeleted = true + } + } + } + + return nil +} + +func UninstallDapr(namespace string) error { + configLoadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + configOverrides := &clientcmd.ConfigOverrides{} + + config := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(configLoadingRules, configOverrides) + + restConfig, err := config.ClientConfig() + + if err != nil { + return err + } + + clientset, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return err + } + + err = clientset.CoreV1().Namespaces().Delete(context.TODO(), "dapr-system", metav1.DeleteOptions{}) + if err != nil { + return err + } + + nsDeleted := false + for !nsDeleted { + list, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return err + } + for _, ns := range list.Items { + // check if the namespace is still there + if ns.Name == "dapr-system" { + fmt.Println("dapr-system namespace is still present. Waiting for it to be deleted") + // wait for 10 seconds + time.Sleep(10 * time.Second) + } else { + nsDeleted = true + } + } + } + + return nil +}