From ab642cbba8c5a170d351f6f23f06c8d7aa025837 Mon Sep 17 00:00:00 2001 From: AdheipSingh <34169002+AdheipSingh@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:06:09 +0530 Subject: [PATCH] feat: uninstaller for Parseable installer (#75) --------- Co-authored-by: Nitish Tiwari --- cmd/uninstaller.go | 28 +++++++ main.go | 20 +++++ pkg/helm/helm.go | 45 ++++++++++ pkg/installer/spinner.go | 27 ++++++ pkg/installer/uninstaller.go | 158 +++++++++++++++++++++++++++++++++++ 5 files changed, 278 insertions(+) create mode 100644 cmd/uninstaller.go create mode 100644 pkg/installer/spinner.go create mode 100644 pkg/installer/uninstaller.go diff --git a/cmd/uninstaller.go b/cmd/uninstaller.go new file mode 100644 index 0000000..e07db54 --- /dev/null +++ b/cmd/uninstaller.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "fmt" + "pb/pkg/common" + "pb/pkg/installer" + + "github.com/spf13/cobra" +) + +var UnInstallOssCmd = &cobra.Command{ + Use: "oss", + Short: "Uninstall Parseable OSS", + Example: "pb uninstall oss", + RunE: func(cmd *cobra.Command, _ []string) error { + // Add verbose flag + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") + + // Print the banner + printBanner() + + if err := installer.Uninstaller(verbose); err != nil { + fmt.Println(common.Red + err.Error()) + } + + return nil + }, +} diff --git a/main.go b/main.go index 6ef86c4..02e8f97 100644 --- a/main.go +++ b/main.go @@ -203,6 +203,23 @@ var install = &cobra.Command{ }, } +var uninstall = &cobra.Command{ + Use: "uninstall", + Short: "Uninstall parseable on kubernetes cluster", + Long: "\nuninstall command is used to uninstall parseable oss/enterprise on k8s cluster..", + PersistentPreRunE: combinedPreRun, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + if os.Getenv("PB_ANALYTICS") == "disable" { + return + } + wg.Add(1) + go func() { + defer wg.Done() + analytics.PostRunAnalytics(cmd, "uninstall", args) + }() + }, +} + func main() { profile.AddCommand(pb.AddProfileCmd) profile.AddCommand(pb.RemoveProfileCmd) @@ -231,6 +248,8 @@ func main() { install.AddCommand(pb.InstallOssCmd) + uninstall.AddCommand(pb.UnInstallOssCmd) + cli.AddCommand(profile) cli.AddCommand(query) cli.AddCommand(stream) @@ -240,6 +259,7 @@ func main() { cli.AddCommand(pb.AutocompleteCmd) cli.AddCommand(install) + cli.AddCommand(uninstall) cli.AddCommand(schema) // Set as command diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index b2bef52..a69d9df 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -350,3 +350,48 @@ func Upgrade(h Helm) error { } return nil } + +func Uninstall(h Helm, verbose bool) (*release.UninstallReleaseResponse, error) { + + // Create a logger that does nothing by default + silentLogger := func(_ string, _ ...interface{}) {} + + // Create settings + settings := cli.New() + + // Create action configuration + actionConfig := new(action.Configuration) + + // Choose logging method based on verbose flag + logMethod := silentLogger + if verbose { + logMethod = log.Printf + } + + // Initialize action configuration with chosen logger + if err := actionConfig.Init( + settings.RESTClientGetter(), + h.Namespace, + os.Getenv("HELM_DRIVER"), + logMethod, + ); err != nil { + return &release.UninstallReleaseResponse{}, fmt.Errorf("failed to initialize Helm configuration: %w", err) + } + + client := action.NewUninstall(actionConfig) + // Setting Namespace + settings.SetNamespace(h.Namespace) + settings.EnvVars() + + settings.EnvVars() + + client.Wait = true + client.Timeout = 5 * time.Minute + + resp, err := client.Run(h.ReleaseName) + if err != nil { + return &release.UninstallReleaseResponse{}, err + } + + return resp, nil +} diff --git a/pkg/installer/spinner.go b/pkg/installer/spinner.go new file mode 100644 index 0000000..38dfb72 --- /dev/null +++ b/pkg/installer/spinner.go @@ -0,0 +1,27 @@ +package installer + +import ( + "fmt" + "pb/pkg/common" + "time" + + "github.com/briandowns/spinner" +) + +func createDeploymentSpinner(namespace, infoMsg string) *spinner.Spinner { + // Custom spinner with multiple character sets for dynamic effect + spinnerChars := []string{ + "●", "○", "◉", "○", "◉", "○", "◉", "○", "◉", + } + + s := spinner.New( + spinnerChars, + 120*time.Millisecond, + spinner.WithColor(common.Yellow), + spinner.WithSuffix(" ..."), + ) + + s.Prefix = fmt.Sprintf(common.Yellow+infoMsg+" %s ", namespace) + + return s +} diff --git a/pkg/installer/uninstaller.go b/pkg/installer/uninstaller.go new file mode 100644 index 0000000..36dc7f7 --- /dev/null +++ b/pkg/installer/uninstaller.go @@ -0,0 +1,158 @@ +package installer + +import ( + "bufio" + "context" + "fmt" + "os" + "path/filepath" + "pb/pkg/common" + "pb/pkg/helm" + "strings" + "time" + + "gopkg.in/yaml.v2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +func Uninstaller(verbose bool) error { + // Load configuration from the parseable.yaml file + configPath := filepath.Join(os.Getenv("HOME"), ".parseable", "parseable.yaml") + config, err := loadParseableConfig(configPath) + if err != nil { + return fmt.Errorf("failed to load configuration: %v", err) + } + + if config == (&ValuesHolder{}) { + return fmt.Errorf("no existing configuration found in ~/.parseable/parseable.yaml") + } + + // Prompt for Kubernetes context + _, err = promptK8sContext() + if err != nil { + return fmt.Errorf("failed to prompt for Kubernetes context: %v", err) + } + + // Prompt user to confirm namespace + namespace := config.ParseableSecret.Namespace + confirm, err := promptUserConfirmation(fmt.Sprintf(common.Yellow+"Do you wish to uninstall Parseable from namespace '%s'?", namespace)) + if err != nil { + return fmt.Errorf("failed to get user confirmation: %v", err) + } + if !confirm { + return fmt.Errorf("Uninstall cancelled.") + } + + // Helm application configuration + helmApp := helm.Helm{ + ReleaseName: "parseable", + Namespace: namespace, + RepoName: "parseable", + RepoURL: "https://charts.parseable.com", + ChartName: "parseable", + Version: "1.6.5", + } + + // Create a spinner + spinner := createDeploymentSpinner(namespace, "Uninstalling parseable in ") + + // Redirect standard output if not in verbose mode + var oldStdout *os.File + if !verbose { + oldStdout = os.Stdout + _, w, _ := os.Pipe() + os.Stdout = w + } + + spinner.Start() + + // Run Helm uninstall + _, err = helm.Uninstall(helmApp, verbose) + spinner.Stop() + + // Restore stdout + if !verbose { + os.Stdout = oldStdout + } + + if err != nil { + return fmt.Errorf("failed to uninstall Parseable: %v", err) + } + + // Namespace cleanup using Kubernetes client + fmt.Printf(common.Yellow+"Cleaning up namespace '%s'...\n"+common.Reset, namespace) + cleanupErr := cleanupNamespaceWithClient(namespace) + if cleanupErr != nil { + return fmt.Errorf("failed to clean up namespace '%s': %v", namespace, cleanupErr) + } + + // Print success banner + fmt.Printf(common.Green+"Successfully uninstalled Parseable from namespace '%s'.\n"+common.Reset, namespace) + + return nil + +} + +// promptUserConfirmation prompts the user for a yes/no confirmation +func promptUserConfirmation(message string) (bool, error) { + reader := bufio.NewReader(os.Stdin) + fmt.Printf("%s [y/N]: ", message) + response, err := reader.ReadString('\n') + if err != nil { + return false, err + } + response = strings.TrimSpace(strings.ToLower(response)) + return response == "y" || response == "yes", nil +} + +// loadParseableConfig loads the configuration from the specified file +func loadParseableConfig(path string) (*ValuesHolder, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var config ValuesHolder + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, err + } + return &config, nil +} + +// cleanupNamespaceWithClient deletes the specified namespace using Kubernetes client-go +func cleanupNamespaceWithClient(namespace string) error { + // Load the kubeconfig + config, err := loadKubeConfig() + if err != nil { + return fmt.Errorf("failed to load kubeconfig: %w", err) + } + + // Create the clientset + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create Kubernetes client: %v", err) + } + + // Create a context with a timeout for namespace deletion + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + // Delete the namespace + err = clientset.CoreV1().Namespaces().Delete(ctx, namespace, v1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("error deleting namespace: %v", err) + } + + // Wait for the namespace to be fully removed + fmt.Printf("Waiting for namespace '%s' to be deleted...\n", namespace) + for { + _, err := clientset.CoreV1().Namespaces().Get(ctx, namespace, v1.GetOptions{}) + if err != nil { + fmt.Printf("Namespace '%s' successfully deleted.\n", namespace) + break + } + time.Sleep(2 * time.Second) + } + + return nil +}