From 1f22051d0fd4321ab02b4e09c3cafb3a123f8ee6 Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Sun, 22 Dec 2024 08:06:15 +0530 Subject: [PATCH] refactor --- cmd/list.go | 60 +----------- cmd/uninstaller.go | 90 +++++++++++++----- main.go | 3 +- pkg/common/common.go | 176 +++++++++++++++++++++++++++++++++++ pkg/installer/installer.go | 57 +----------- pkg/installer/model.go | 8 -- pkg/installer/spinner.go | 43 --------- pkg/installer/uninstaller.go | 4 +- 8 files changed, 253 insertions(+), 188 deletions(-) delete mode 100644 pkg/installer/spinner.go diff --git a/cmd/list.go b/cmd/list.go index 7fac778..4830138 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -1,21 +1,14 @@ package cmd import ( - "context" "fmt" "log" "os" "pb/pkg/common" - "pb/pkg/installer" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" ) // ListOssCmd lists the Parseable OSS servers @@ -24,13 +17,13 @@ var ListOssCmd = &cobra.Command{ Short: "List available Parseable OSS servers", Example: "pb list oss", Run: func(cmd *cobra.Command, _ []string) { - _, err := installer.PromptK8sContext() + _, err := common.PromptK8sContext() if err != nil { log.Fatalf("Failed to prompt for kubernetes context: %v", err) } // Read the installer data from the ConfigMap - entries, err := readInstallerConfigMap() + entries, err := common.ReadInstallerConfigMap() if err != nil { log.Fatalf("Failed to list OSS servers: %v", err) } @@ -52,52 +45,3 @@ var ListOssCmd = &cobra.Command{ table.Render() }, } - -// readInstallerConfigMap fetches and parses installer data from a ConfigMap -func readInstallerConfigMap() ([]installer.InstallerEntry, error) { - const ( - configMapName = "parseable-installer" - namespace = "pb-system" - dataKey = "installer-data" - ) - - // Load kubeconfig and create a Kubernetes client - config, err := loadKubeConfig() - if err != nil { - return nil, fmt.Errorf("failed to load kubeconfig: %w", err) - } - - clientset, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to create Kubernetes client: %w", err) - } - - // Get the ConfigMap - cm, err := clientset.CoreV1().ConfigMaps(namespace).Get(context.TODO(), configMapName, metav1.GetOptions{}) - if err != nil { - return nil, fmt.Errorf("failed to fetch ConfigMap: %w", err) - } - - // Retrieve and parse the installer data - rawData, ok := cm.Data[dataKey] - if !ok { - fmt.Println(common.Yellow + "\n────────────────────────────────────────────────────────────────────────────") - fmt.Println(common.Yellow + "⚠️ No Parseable clusters found!") - fmt.Println(common.Yellow + "To get started, run: `pb install oss`") - fmt.Println(common.Yellow + "────────────────────────────────────────────────────────────────────────────\n") - return nil, nil - } - - var entries []installer.InstallerEntry - if err := yaml.Unmarshal([]byte(rawData), &entries); err != nil { - return nil, fmt.Errorf("failed to parse ConfigMap data: %w", err) - } - - return entries, nil -} - -// loadKubeConfig loads the kubeconfig from the default location -func loadKubeConfig() (*rest.Config, error) { - kubeconfig := clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename() - return clientcmd.BuildConfigFromFlags("", kubeconfig) -} diff --git a/cmd/uninstaller.go b/cmd/uninstaller.go index 64cdec5..0d770b8 100644 --- a/cmd/uninstaller.go +++ b/cmd/uninstaller.go @@ -1,40 +1,82 @@ -// Copyright (c) 2024 Parseable, Inc -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - package cmd import ( "fmt" + "log" + "pb/pkg/common" - "pb/pkg/installer" + "pb/pkg/helm" "github.com/spf13/cobra" ) -var UnInstallOssCmd = &cobra.Command{ +// UninstallOssCmd removes Parseable OSS servers +var UninstallOssCmd = &cobra.Command{ Use: "oss", - Short: "Uninstall Parseable OSS", + Short: "Uninstall Parseable OSS servers", Example: "pb uninstall oss", - RunE: func(cmd *cobra.Command, _ []string) error { - // Add verbose flag - cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") + Run: func(cmd *cobra.Command, _ []string) { + _, err := common.PromptK8sContext() + if err != nil { + log.Fatalf("Failed to prompt for Kubernetes context: %v", err) + } + + // Read the installer data from the ConfigMap + entries, err := common.ReadInstallerConfigMap() + if err != nil { + log.Fatalf("Failed to fetch OSS servers: %v", err) + } + + // Check if there are no entries + if len(entries) == 0 { + fmt.Println(common.Yellow + "\nNo Parseable OSS servers found to uninstall.\n") + return + } + + // Prompt user to select a cluster + selectedCluster, err := common.PromptClusterSelection(entries) + if err != nil { + log.Fatalf("Failed to select a cluster: %v", err) + } - if err := installer.Uninstaller(verbose); err != nil { - fmt.Println(common.Red + err.Error()) + // Confirm uninstallation + fmt.Printf("\nYou have selected to uninstall the cluster '%s' in namespace '%s'.\n", selectedCluster.Name, selectedCluster.Namespace) + if !common.PromptConfirmation(fmt.Sprintf("Do you want to proceed with uninstalling '%s'?", selectedCluster.Name)) { + fmt.Println(common.Yellow + "Uninstall operation canceled.") + return } - return nil + // Perform uninstallation + if err := uninstallCluster(selectedCluster); err != nil { + log.Fatalf("Failed to uninstall cluster: %v", err) + } + + fmt.Println(common.Green + "Uninstallation completed successfully." + common.Reset) }, } + +func uninstallCluster(entry common.InstallerEntry) error { + helmApp := helm.Helm{ + ReleaseName: entry.Name, + Namespace: entry.Namespace, + RepoName: "parseable", + RepoURL: "https://charts.parseable.com", + ChartName: "parseable", + Version: entry.Version, + } + + fmt.Println(common.Yellow + "Starting uninstallation process..." + common.Reset) + + spinner := common.CreateDeploymentSpinner(entry.Namespace, fmt.Sprintf("Uninstalling Parseable OSS '%s'...", entry.Name)) + spinner.Start() + + _, err := helm.Uninstall(helmApp, false) + spinner.Stop() + + if err != nil { + return fmt.Errorf("failed to uninstall Parseable OSS: %v", err) + } + + fmt.Printf(common.Green+"Successfully uninstalled '%s' from namespace '%s'.\n"+common.Reset, entry.Name, entry.Namespace) + return nil +} diff --git a/main.go b/main.go index 11f06d4..a6bb0c5 100644 --- a/main.go +++ b/main.go @@ -264,9 +264,10 @@ func main() { schema.AddCommand(pb.CreateSchemaCmd) install.AddCommand(pb.InstallOssCmd) + list.AddCommand(pb.ListOssCmd) - uninstall.AddCommand(pb.UnInstallOssCmd) + uninstall.AddCommand(pb.UninstallOssCmd) cli.AddCommand(profile) cli.AddCommand(query) diff --git a/pkg/common/common.go b/pkg/common/common.go index 56271d2..0fe5b13 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -15,6 +15,22 @@ package common +import ( + "context" + "fmt" + "os" + "time" + + "github.com/briandowns/spinner" + "github.com/manifoldco/promptui" + "gopkg.in/yaml.v2" + apiErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + // ANSI escape codes for colors const ( Yellow = "\033[33m" @@ -24,3 +40,163 @@ const ( Blue = "\033[34m" Cyan = "\033[36m" ) + +// InstallerEntry represents an entry in the installer.yaml file +type InstallerEntry struct { + Name string `yaml:"name"` + Namespace string `yaml:"namespace"` + Version string `yaml:"version"` + Status string `yaml:"status"` // todo ideally should be a heartbeat +} + +// ReadInstallerConfigMap fetches and parses installer data from a ConfigMap +func ReadInstallerConfigMap() ([]InstallerEntry, error) { + const ( + configMapName = "parseable-installer" + namespace = "pb-system" + dataKey = "installer-data" + ) + + // Load kubeconfig and create a Kubernetes client + config, err := LoadKubeConfig() + if err != nil { + return nil, fmt.Errorf("failed to load kubeconfig: %w", err) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("failed to create Kubernetes client: %w", err) + } + + // Get the ConfigMap + cm, err := clientset.CoreV1().ConfigMaps(namespace).Get(context.TODO(), configMapName, metav1.GetOptions{}) + if err != nil { + if apiErrors.IsNotFound(err) { + fmt.Println(Yellow + "\nNo existing Parseable OSS clusters found.\n" + Reset) + return nil, nil + } + return nil, fmt.Errorf("failed to fetch ConfigMap: %w", err) + } + // Retrieve and parse the installer data + rawData, ok := cm.Data[dataKey] + if !ok { + fmt.Println(Yellow + "\n────────────────────────────────────────────────────────────────────────────") + fmt.Println(Yellow + "⚠️ No Parseable clusters found!") + fmt.Println(Yellow + "To get started, run: `pb install oss`") + fmt.Println(Yellow + "────────────────────────────────────────────────────────────────────────────\n") + return nil, nil + } + + var entries []InstallerEntry + if err := yaml.Unmarshal([]byte(rawData), &entries); err != nil { + return nil, fmt.Errorf("failed to parse ConfigMap data: %w", err) + } + + return entries, nil +} + +// LoadKubeConfig loads the kubeconfig from the default location +func LoadKubeConfig() (*rest.Config, error) { + kubeconfig := clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename() + return clientcmd.BuildConfigFromFlags("", kubeconfig) +} + +// PromptK8sContext retrieves Kubernetes contexts from kubeconfig. +func PromptK8sContext() (clusterName string, err error) { + kubeconfigPath := os.Getenv("KUBECONFIG") + if kubeconfigPath == "" { + kubeconfigPath = os.Getenv("HOME") + "/.kube/config" + } + + // Load kubeconfig file + config, err := clientcmd.LoadFromFile(kubeconfigPath) + if err != nil { + fmt.Printf("\033[31mError loading kubeconfig: %v\033[0m\n", err) + os.Exit(1) + } + + // Get current contexts + currentContext := config.Contexts + var contexts []string + for i := range currentContext { + contexts = append(contexts, i) + } + + // Prompt user to select Kubernetes context + promptK8s := promptui.Select{ + Items: contexts, + Templates: &promptui.SelectTemplates{ + Label: "{{ `Select your Kubernetes context` | yellow }}", + Active: "▸ {{ . | yellow }} ", // Yellow arrow and context name for active selection + Inactive: " {{ . | yellow }}", // Default color for inactive items + Selected: "{{ `Selected Kubernetes context:` | green }} '{{ . | green }}' ✔", + }, + } + + _, clusterName, err = promptK8s.Run() + if err != nil { + return "", err + } + + // Set current context as selected + config.CurrentContext = clusterName + err = clientcmd.WriteToFile(*config, kubeconfigPath) + if err != nil { + return "", err + } + + return clusterName, nil +} + +func PromptClusterSelection(entries []InstallerEntry) (InstallerEntry, error) { + clusterNames := make([]string, len(entries)) + for i, entry := range entries { + clusterNames[i] = fmt.Sprintf("[Name: %s] [Namespace: %s] [Version: %s]", entry.Name, entry.Namespace, entry.Version) + } + + prompt := promptui.Select{ + Label: "Select a cluster to uninstall", + Items: clusterNames, + Templates: &promptui.SelectTemplates{ + Label: "{{ `Select Cluster` | yellow }}", + Active: "▸ {{ . | yellow }}", + Inactive: " {{ . | yellow }}", + Selected: "{{ `Selected:` | green }} {{ . | green }}", + }, + } + + index, _, err := prompt.Run() + if err != nil { + return InstallerEntry{}, fmt.Errorf("failed to prompt for cluster selection: %v", err) + } + + return entries[index], nil +} + +func PromptConfirmation(message string) bool { + prompt := promptui.Prompt{ + Label: message, + IsConfirm: true, + } + + _, err := prompt.Run() + return err == nil +} + +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(Yellow), + spinner.WithSuffix(" ..."), + ) + + s.Prefix = fmt.Sprintf(Yellow + infoMsg) + + return s +} diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index a45c205..e85354f 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -63,7 +63,7 @@ func waterFall(verbose bool) { log.Fatalf("Failed to prompt for plan selection: %v", err) } - _, err = PromptK8sContext() + _, err = common.PromptK8sContext() if err != nil { log.Fatalf("Failed to prompt for kubernetes context: %v", err) } @@ -111,7 +111,7 @@ func waterFall(verbose bool) { Verbose: verbose, } - if err := updateInstallerConfigMap(InstallerEntry{ + if err := updateInstallerConfigMap(common.InstallerEntry{ Name: pbInfo.Name, Namespace: pbInfo.Namespace, Version: config.Version, @@ -636,53 +636,6 @@ func promptForInput(label string) string { return strings.TrimSpace(input) } -// PromptK8sContext retrieves Kubernetes contexts from kubeconfig. -func PromptK8sContext() (clusterName string, err error) { - kubeconfigPath := os.Getenv("KUBECONFIG") - if kubeconfigPath == "" { - kubeconfigPath = os.Getenv("HOME") + "/.kube/config" - } - - // Load kubeconfig file - config, err := clientcmd.LoadFromFile(kubeconfigPath) - if err != nil { - fmt.Printf("\033[31mError loading kubeconfig: %v\033[0m\n", err) - os.Exit(1) - } - - // Get current contexts - currentContext := config.Contexts - var contexts []string - for i := range currentContext { - contexts = append(contexts, i) - } - - // Prompt user to select Kubernetes context - promptK8s := promptui.Select{ - Items: contexts, - Templates: &promptui.SelectTemplates{ - Label: "{{ `Select your Kubernetes context` | yellow }}", - Active: "▸ {{ . | yellow }} ", // Yellow arrow and context name for active selection - Inactive: " {{ . | yellow }}", // Default color for inactive items - Selected: "{{ `Selected Kubernetes context:` | green }} '{{ . | green }}' ✔", - }, - } - - _, clusterName, err = promptK8s.Run() - if err != nil { - return "", err - } - - // Set current context as selected - config.CurrentContext = clusterName - err = clientcmd.WriteToFile(*config, kubeconfigPath) - if err != nil { - return "", err - } - - return clusterName, nil -} - // printBanner displays a welcome banner func printBanner() { banner := ` @@ -719,7 +672,7 @@ func deployRelease(config HelmDeploymentConfig) error { // Create a spinner msg := fmt.Sprintf(" Deploying parseable release name [%s] namespace [%s] ", config.ReleaseName, config.Namespace) - spinner := createDeploymentSpinner(config.Namespace, msg) + spinner := common.CreateDeploymentSpinner(config.Namespace, msg) // Redirect standard output if not in verbose mode var oldStdout *os.File @@ -867,7 +820,7 @@ func openBrowser(url string) { cmd.Start() } -func updateInstallerConfigMap(entry InstallerEntry) error { +func updateInstallerConfigMap(entry common.InstallerEntry) error { const ( configMapName = "parseable-installer" namespace = "pb-system" @@ -936,7 +889,7 @@ func updateInstallerConfigMap(entry InstallerEntry) error { // Retrieve existing data and append the new entry existingData := data["data"].(map[string]interface{}) - var entries []InstallerEntry + var entries []common.InstallerEntry if raw, ok := existingData[dataKey]; ok { if err := yaml.Unmarshal([]byte(raw.(string)), &entries); err != nil { return fmt.Errorf("failed to parse existing ConfigMap data: %v", err) diff --git a/pkg/installer/model.go b/pkg/installer/model.go index cbb09a6..84d08c4 100644 --- a/pkg/installer/model.go +++ b/pkg/installer/model.go @@ -93,11 +93,3 @@ type Blob struct { Container string // Container name in the Azure Blob store. URL string // URL of the Azure Blob store. } - -// InstallerEntry represents an entry in the installer.yaml file -type InstallerEntry struct { - Name string `yaml:"name"` - Namespace string `yaml:"namespace"` - Version string `yaml:"version"` - Status string `yaml:"status"` // todo ideally should be a heartbeat -} diff --git a/pkg/installer/spinner.go b/pkg/installer/spinner.go deleted file mode 100644 index a53fbf2..0000000 --- a/pkg/installer/spinner.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2024 Parseable, Inc -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package installer - -import ( - "fmt" - "time" - - "pb/pkg/common" - - "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) - - return s -} diff --git a/pkg/installer/uninstaller.go b/pkg/installer/uninstaller.go index 005d8d5..ea9ef6e 100644 --- a/pkg/installer/uninstaller.go +++ b/pkg/installer/uninstaller.go @@ -49,7 +49,7 @@ func Uninstaller(verbose bool) error { } // Unmarshal the installer file content - var entries []InstallerEntry + var entries []common.InstallerEntry if err := yaml.Unmarshal(data, &entries); err != nil { return fmt.Errorf("failed to parse installer file: %w", err) } @@ -105,7 +105,7 @@ func Uninstaller(verbose bool) error { } // Create a spinner - spinner := createDeploymentSpinner(selectedCluster.Namespace, "Uninstalling Parseable in ") + spinner := common.CreateDeploymentSpinner(selectedCluster.Namespace, "Uninstalling Parseable in ") // Redirect standard output if not in verbose mode var oldStdout *os.File